wandres.dev
RECORTE Y MÁSCARAS · clip-path, mask, shape-outside

clip-path y el catálogo de formas básicas

Las cinco funciones de forma, cómo se resuelven sus porcentajes, qué caja de referencia usa cada una, y las dos reglas que deciden si dos formas se pueden animar entre sí.

⏱ 18 min

clip-path recorta un elemento a una región geométrica y descarta todo lo que quede fuera, contenido incluido. Es una operación binaria, barata y con dos detalles que decepcionan a quien la descubre: los porcentajes de circle() no se resuelven contra lo que crees, y dos formas solo se animan entre sí si cumplen una condición bastante estricta.

🎯 Al terminar esta lección sabrás
  • Escribir las cinco funciones de forma con su sintaxis completa.
  • Resolver los porcentajes de cada una contra la caja correcta.
  • Cambiar la caja de referencia y saber cuál es la de por defecto.
  • Predecir si una transición entre dos formas va a funcionar.

Las cinco funciones

inset() recorta un rectángulo desde los cuatro bordes hacia dentro, con la misma abreviatura de cuatro valores que margin, y admite esquinas redondeadas:

.a { clip-path: inset(20px); }
.b { clip-path: inset(10% 20% 30% 40%); }
.c { clip-path: inset(20px round 12px); }
.d { clip-path: inset(20px round 40px / 20px); }

La palabra round va seguida de una especificación de radio idéntica a la de border-radius, barra incluida.

circle() toma un radio y una posición:

.a { clip-path: circle(50%); }
.b { clip-path: circle(80px at 30% 20%); }
.c { clip-path: circle(closest-side at center); }

Y aquí está la sorpresa: el porcentaje del radio no se resuelve contra el ancho ni contra el alto. Se resuelve contra la diagonal de la caja dividida por la raíz de dos, es decir contra sqrt(w² + h²) / sqrt(2). En una caja cuadrada eso coincide con el lado y circle(50%) toca los cuatro bordes; en una caja rectangular queda entre el lado corto y el largo. La regla existe para que el círculo crezca de forma proporcionada en cualquier proporción de caja, y para que circle(50%) en un cuadrado dé exactamente el círculo inscrito.

Las palabras closest-side y farthest-side evitan el cálculo: producen el círculo que toca el lado más cercano o el más lejano al centro elegido.

ellipse() toma dos radios, uno por eje, con los porcentajes resueltos de forma normal contra ancho y alto:

.a { clip-path: ellipse(50% 30%); }
.b { clip-path: ellipse(120px 60px at 50% 100%); }

polygon() toma una lista de pares de coordenadas, y opcionalmente una regla de relleno:

.flecha { clip-path: polygon(0 0, 75% 0, 100% 50%, 75% 100%, 0 100%); }
.estrella {
  clip-path: polygon(evenodd,
    50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%,
    50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}

Los porcentajes son relativos al ancho para el primer valor de cada par y al alto para el segundo. La regla de relleno, nonzero por defecto o evenodd, decide qué se considera “dentro” cuando el polígono se cruza a sí mismo, exactamente igual que en SVG.

path() toma una cadena con datos de ruta SVG, y aparece en la lección siguiente porque tiene suficiente sustancia propia.

La caja de referencia

Todas las funciones se resuelven contra una caja, y esa caja se puede cambiar añadiendo una palabra clave:

.a { clip-path: circle(50%) content-box; }
.b { clip-path: inset(0) margin-box; }

Las cajas disponibles son margin-box, border-box, padding-box y content-box para elementos HTML, y fill-box, stroke-box y view-box para contenido SVG. Para un elemento HTML el valor por defecto es border-box.

Cambiarla es más útil de lo que parece. Un elemento con relleno recortado con content-box deja el relleno fuera del recorte, con lo que la forma se ajusta al contenido real y no a la caja. Y margin-box permite que la forma se extienda más allá del elemento, cosa que ninguna otra propiedad hace.

También se puede usar la palabra de caja sola, sin función de forma:

/* Recorta a la caja de relleno, siguiendo el border-radius. */
.a { clip-path: padding-box; }

Eso recorta a esa caja respetando el border-radius, y es la forma más corta de recortar los hijos a la curva del padre sin usar overflow: hidden y sin las consecuencias de crear un contexto de desplazamiento.

Qué se puede animar

clip-path es animable, con dos condiciones que se cumplen menos veces de las que se querría.

Las dos formas tienen que usar la misma función. No hay interpolación entre circle() y polygon(), ni entre inset() y ellipse(). El cambio es un salto.

Con polygon(), las dos listas tienen que tener el mismo número de vértices y la misma regla de relleno. Si una tiene cinco puntos y otra seis, salto.

De esa segunda regla sale la técnica que se usa siempre: repetir vértices para igualar el número de puntos. Un triángulo que se convierte en un cuadrado se escribe como dos polígonos de cuatro puntos, con dos puntos del triángulo en la misma posición.

.forma {
  clip-path: polygon(50% 0%, 100% 100%, 0% 100%, 0% 100%);
  transition: clip-path 400ms;
}
.forma:hover {
  clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}

El cuarto punto del triángulo está duplicado sobre el tercero. Al animar, se separa y se convierte en la cuarta esquina del cuadrado. Es un truco viejo de la animación por morphing y funciona igual de bien aquí.

ℹ️
clip-path crea un contexto de apilamiento

Un clip-path distinto de none crea contexto de apilamiento, igual que filter o opacity menor que 1. Ningún z-index de dentro del elemento puede escapar de él, y eso a veces explica por qué un menú desplegable se queda debajo de un hermano después de añadir un recorte que no tenía nada que ver.

El recorte no reduce el trabajo, y por eso no es una optimización

Hay una intuición razonable y equivocada sobre clip-path: que recortar reduce lo que hay que pintar y por tanto ahorra. No ahorra. El recorte se aplica después de que el elemento y su subárbol se hayan pintado; lo que hace es descartar píxeles ya calculados, no evitar calcularlos. Recortar un elemento al 10% de su superficie cuesta el pintado completo más la operación de recorte, es decir un poco más que no recortar nada. La confusión viene de mezclarlo con overflow: hidden, que tampoco ahorra pintado pero sí evita que aparezcan barras y sí permite ciertas optimizaciones de scroll, y sobre todo con content-visibility, que es la única propiedad de esta familia que de verdad evita trabajo porque le dice al navegador que se salte el subárbol entero. La consecuencia práctica más útil es sobre el texto recortado a medias. Cuando un clip-path corta un párrafo por la mitad, ese texto se ha maquetado entero, se ha pintado entero y sigue estando en el árbol de accesibilidad entero: un lector de pantalla lo lee completo y una búsqueda en la página lo encuentra. Eso es correcto y a veces es justo lo que quieres —una tarjeta con una forma decorativa cuyo contenido sigue siendo accesible— y a veces es un problema serio, porque el usuario que oye el texto no entiende por qué el que lo ve solo percibe la mitad. La regla de higiene es corta: clip-path es una herramienta de forma, nunca de ocultación. Si algo no debe estar disponible, se quita del árbol con display: none o con hidden, no se recorta. Y si algo se recorta por diseño, asegúrate de que lo recortado es decorativo y de que lo que hace falta entender sigue estando dentro de la región visible.

⚔️ Recorta con precisión
  1. Aplica circle(50%) a una caja cuadrada y a una de 3 por 1 y compara los radios resultantes.
  2. Recorta un elemento con relleno usando content-box y border-box y observa la diferencia.
  3. Anima un triángulo a un cuadrado duplicando vértices.
  4. Prueba a animar de circle() a polygon() y comprueba que salta.
  5. Recorta un párrafo por la mitad y léelo con un lector de pantalla.