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

path(), shape() y las formas de coordenadas

Los datos de ruta SVG dentro de CSS y su gran limitación, las funciones rect() y xywh(), y qué aporta shape() cuando el soporte lo permite.

⏱ 17 min

Las formas básicas cubren el noventa por ciento de los casos y dejan fuera todo lo que tenga una curva que no sea un arco de circunferencia. Para eso está path(), que mete datos de ruta SVG dentro de CSS con una limitación que lo estropea casi todo, y shape(), que existe precisamente para arreglar esa limitación y todavía no está en todas partes.

🎯 Al terminar esta lección sabrás
  • Escribir un path() con datos de ruta SVG dentro de una declaración CSS.
  • Explicar por qué path() no responde al tamaño del elemento.
  • Usar rect() y xywh() con detección de soporte.
  • Valorar qué aporta shape() y cómo protegerlo con @supports.

path() y sus coordenadas absolutas

path() acepta una cadena con la misma sintaxis del atributo d de un <path> de SVG:

.gota {
  clip-path: path("M 100 0 C 155 0 200 45 200 100 C 200 155 155 200 100 200 C 45 200 0 155 0 100 C 0 45 45 0 100 0 Z");
}

Los comandos son los de siempre: M para mover, L para línea, C y S para curvas cúbicas, Q y T para cuadráticas, A para arcos elípticos, Z para cerrar. En minúscula, las mismas órdenes en coordenadas relativas.

Y aquí está el problema que decide cuándo se puede usar: las coordenadas de path() son píxeles absolutos. No hay porcentajes, no hay unidades, no hay calc(). La ruta del ejemplo dibuja una forma de 200 por 200 píxeles, y la dibujará igual en un elemento de 200 píxeles y en uno de 800: en el segundo recortará solo la esquina superior izquierda.

Eso convierte a path() en una herramienta para elementos de tamaño fijo y poco más. Cualquier componente que se adapte al contenido o al viewport queda fuera.

Hay un caso donde sí es la respuesta correcta, y es cuando la forma la genera una herramienta de diseño y el elemento tiene tamaño conocido: un icono, un sello, un separador decorativo de altura fija.

path() acepta también la regla de relleno delante de la cadena:

.anillo { clip-path: path(evenodd, "M 0 0 H 200 V 200 H 0 Z M 50 50 H 150 V 150 H 50 Z"); }

Dos subrutas, la exterior y la interior, con evenodd produciendo el agujero. Es la forma de recortar un anillo o un marco, cosa que ninguna forma básica permite.

⚠️
path() se usa en tres propiedades y solo en una tiene sentido escalar

Los mismos datos de ruta valen en clip-path, en offset-path para mover un elemento a lo largo de una trayectoria, y en d de un <path> SVG. En las dos primeras las coordenadas son píxeles CSS absolutos; en la tercera son unidades del sistema de coordenadas del SVG, que sí escala con el viewBox. Si necesitas una forma compleja que escale, la ruta correcta sigue siendo un SVG con viewBox y clip-path: url(#id).

rect() y xywh()

Dos funciones que hacen lo mismo que inset() con un sistema de coordenadas distinto, y que resultan mucho más naturales cuando la forma viene de un cálculo.

/* Desde los bordes superior, derecho, inferior e izquierdo. */
.a { clip-path: rect(10px 90% 90% 10px round 8px); }

/* Desde la esquina superior izquierda, con ancho y alto. */
.b { clip-path: xywh(10px 10px 200px 120px round 8px); }

inset() mide desde los cuatro bordes hacia dentro, que es cómodo para márgenes y espantoso para colocar un rectángulo en una posición conocida: el valor derecho e inferior hay que calcularlos como el total menos lo que quieres. rect() mide las cuatro aristas desde los bordes superior e izquierdo, y xywh() da origen y tamaño, que es como piensa cualquiera que venga de un canvas o de un sistema de coordenadas.

Las dos aceptan auto en cualquier posición, con el significado de “el borde de la caja de referencia”, y las dos admiten round.

Son recientes, así que conviene protegerlas:

.recorte {
  clip-path: inset(10px calc(100% - 210px) calc(100% - 130px) 10px round 8px);
}

@supports (clip-path: xywh(0 0 1px 1px)) {
  .recorte { clip-path: xywh(10px 10px 200px 120px round 8px); }
}

shape() y lo que arregla

shape() es la respuesta de la especificación al problema de path(). Describe la misma clase de geometría —líneas, curvas, arcos— pero con la sintaxis y las unidades de CSS, lo cual significa porcentajes, calc(), custom properties y todo lo demás.

.hoja {
  clip-path: shape(
    from 0% 0%,
    curve to 100% 100% with 100% 0%,
    curve to 0% 0% with 0% 100%
  );
}

Los comandos son move, line, hline, vline, curve to ... with ... para curvas de Bézier, smooth, arc, y close. Cada coordenada es un par de valores de CSS, con lo que una forma puede depender del tamaño del elemento, de una custom property o de una consulta de contenedor.

La segunda ventaja es la animación. Dos shape() con la misma lista de comandos se interpolan, y como los valores son de CSS se pueden interpolar contra porcentajes distintos. Con path() eso no existe.

El soporte no es universal: llegó a los motores basados en Chromium y a Safari durante 2025, y no está en los cuatro. Escríbelo siempre detrás de una comprobación:

.hoja {
  clip-path: ellipse(50% 50%);
}

@supports (clip-path: shape(from 0% 0%, line to 100% 100%)) {
  .hoja {
    clip-path: shape(
      from 0% 0%,
      curve to 100% 100% with 100% 0%,
      curve to 0% 0% with 0% 100%
    );
  }
}

Un @supports con una forma mínima pero sintácticamente completa es la comprobación correcta: si el navegador no entiende la función, la consulta falla y se queda el ellipse().

Elegir entre CSS y SVG para una forma no es una cuestión de gusto, es de qué escala con qué

Hay cuatro formas de recortar un elemento con una geometría no trivial y cada una tiene un régimen donde es la única correcta. La confusión viene de compararlas por comodidad sintáctica en vez de por su comportamiento frente al tamaño. polygon() con porcentajes escala en los dos ejes de forma independiente, es decir, deforma: una flecha en un elemento que se ensancha se ensancha con él y su punta se vuelve más obtusa. A veces eso es exactamente lo que quieres —un bisel, un corte diagonal— y a veces destruye la forma. path() no escala en absoluto: coordenadas absolutas, la forma es un tamaño fijo dentro de una caja variable, con lo que la relación entre ambas cambia y casi nunca de forma útil. shape() escala como tú decidas, porque cada coordenada es una expresión de CSS y puedes mezclar porcentajes con longitudes: curve to 100% 40px with calc(100% - 20px) 0% mantiene un detalle de tamaño fijo mientras el resto se adapta, que es lo que ninguna de las otras dos permite y lo que casi siempre se necesita en un componente real. Y clip-path: url(#id) apuntando a un <clipPath> de SVG escala uniformemente si el SVG usa clipPathUnits="objectBoundingBox", es decir, mantiene las proporciones de la forma aunque el elemento cambie de proporción, cosa que ninguna función de CSS sabe hacer. Esa última es la que se olvida siempre y la que resuelve el caso de “quiero exactamente esta forma, sin deformar, en un contenedor de proporción desconocida”. El coste es tener un SVG en el documento y una referencia por identificador, con todo lo que eso implica cuando el componente se mueve entre documentos o entre shadow roots. El criterio entero cabe en una pregunta: qué quiero que le pase a esta forma cuando la caja cambie de proporción, y según la respuesta —que se deforme, que no se mueva, que se adapte por partes o que se escale uniformemente— hay exactamente una herramienta correcta.

⚔️ Formas que escalan
  1. Recorta con path() un elemento y cambia su ancho. Observa qué pasa con la forma.
  2. Reescribe la misma forma con polygon() en porcentajes y compara la deformación.
  3. Coloca un rectángulo interior con inset() y con xywh() y compara la legibilidad del código.
  4. Escribe una forma con shape() mezclando porcentajes y longitudes, protegida con @supports.
  5. Prueba un clipPath de SVG con clipPathUnits de caja delimitadora y verifica que no deforma.