wandres.dev
RECORTE Y MÁSCARA · clipPath y mask en SVG

Recortar y enmascarar desde CSS

clip-path y mask sobre elementos HTML: las formas básicas, el problema de escala de path(), la función shape(), y las diferencias de comportamiento respecto al uso desde SVG.

⏱ 17 min

clip-path y mask son propiedades CSS, no atributos de SVG, y funcionan sobre cualquier elemento del documento. Eso convierte todo lo aprendido en este nivel en una herramienta de maquetación: cortar una imagen en diagonal, difuminar el borde de una lista con scroll, recortar un vídeo con la forma de un logotipo. Pero las funciones de CSS tienen su propia geometría, sus propias unidades y una limitación importante en path() que la función shape() vino a resolver.

🎯 Al terminar esta lección sabrás
  • Aplicar clip-path con formas básicas y con una referencia a un clipPath de SVG.
  • Explicar por qué path() no escala con el elemento y qué hace shape() distinto.
  • Usar mask con un degradado y con un fichero SVG externo.
  • Enumerar las diferencias de comportamiento entre el uso desde CSS y desde SVG.

clip-path con formas básicas

La propiedad acepta un conjunto de funciones de forma, con coordenadas relativas a la caja de referencia del elemento:

.diagonal  { clip-path: polygon(0 0, 100% 0, 100% 80%, 0 100%); }
.circulo   { clip-path: circle(50%); }
.elipse    { clip-path: ellipse(40% 50% at 30% 50%); }
.recuadro  { clip-path: inset(10px 20px round 8px); }
.rectangulo{ clip-path: rect(0 100% 60% 0 round 12px); }
.xywh      { clip-path: xywh(0 0 100% 60% round 12px); }

Los porcentajes se resuelven contra la caja de referencia, que por omisión es border-box y se puede cambiar añadiendo una palabra clave: clip-path: circle(50%) content-box.

circle() con un porcentaje merece una nota: el radio no se resuelve contra el ancho ni contra el alto, sino contra la diagonal normalizada de la caja de referencia, exactamente la misma fórmula que se explicó para el atributo r de un circle de SVG. En una caja no cuadrada, circle(50%) no es media anchura.

Estas funciones son interpolables entre sí si tienen la misma estructura, lo que permite transiciones:

.tarjeta { clip-path: inset(0); transition: clip-path 300ms ease; }
.tarjeta:hover { clip-path: inset(0 0 0 0 round 24px); }

Un polygon() transiciona con otro polygon() si tienen el mismo número de vértices. Es la restricción que obliga a escribir polígonos con vértices redundantes cuando se quiere animar entre formas de complejidad distinta.

Referenciar un clipPath de SVG

clip-path: url(#forma) funciona sobre un elemento HTML, apuntando a un clipPath que esté en un SVG del documento. Con una condición práctica importante: el clipPath tiene que estar en unidades adecuadas.

<svg width="0" height="0" style="position:absolute">
  <clipPath id="estrella" clipPathUnits="objectBoundingBox">
    <polygon points="0.5,0 0.61,0.35 1,0.35 0.68,0.57 0.79,0.91
                     0.5,0.7 0.21,0.91 0.32,0.57 0,0.35 0.39,0.35" />
  </clipPath>
</svg>
.foto { clip-path: url(#estrella); }

Con objectBoundingBox el recorte se adapta al tamaño del elemento HTML. Con userSpaceOnUse las coordenadas serían píxeles absolutos y el recorte solo funcionaría para un tamaño concreto.

La referencia tiene que ser a un clipPath del mismo documento. Un url(recortes.svg#estrella) para clip-path no funciona en todos los motores; la referencia externa es fiable para mask y no lo es para clip-path.

El problema de path() y la solución shape()

clip-path: path("M 0 0 L 100 0 L 100 100 Z") acepta la sintaxis del atributo d, y trae una limitación grave: las coordenadas son píxeles absolutos y no escalan con el elemento. Un recorte diseñado para una tarjeta de 300 píxeles se queda pequeño en una de 600. No hay porcentajes, no hay unidades relativas, no hay nada.

La función shape() resuelve exactamente eso. Usa una sintaxis de comandos en palabras, y admite cualquier unidad de CSS, incluidos porcentajes y calc():

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

Cada porcentaje se resuelve contra la caja de referencia, así que la forma escala. Es la función que hace que los recortes complejos sean usables en un diseño responsivo, y la que sustituye a path() en todo salvo en el caso de una forma de tamaño fijo.

Su disponibilidad no es universal: está en los motores basados en Chromium y en WebKit desde 2025, y no en todos. La degradación es dura (sin soporte, la declaración se descarta y no hay recorte), así que si la usas conviene una detección:

.hoja { clip-path: polygon(0 50%, 50% 0, 100% 50%); }

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

mask desde CSS

La propiedad mask es una abreviatura con la misma estructura que background: imagen, posición, tamaño, repetición, origen, recorte y modo.

.lista {
  mask: linear-gradient(to bottom, #000 0 85%, transparent 100%);
}

.icono {
  background-color: currentColor;
  mask: url(/iconos/estrella.svg) center / contain no-repeat;
}

.foto {
  mask-image: url(#suave);          /* un mask de SVG del documento */
}

Diferencias con SVG que hay que tener presentes:

El modo por omisión es alfa, no luminancia. Cuando la máscara es una imagen o un degradado de CSS, mask-mode vale match-source, que para una imagen significa alfa. Solo si la fuente es un mask de SVG se usa el modo que ese elemento declare. Esta inversión respecto a SVG es la causa de que un degradado a transparent funcione en CSS y uno a transparent en una máscara de SVG dé un resultado distinto.

Se puede componer varias máscaras. mask-composite con add, subtract, intersect y exclude permite combinar capas, algo que en SVG requiere filtros.

.marco {
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
}

Ese patrón produce un marco: la máscara del contenido se resta de la total, dejando solo el borde. Es la técnica estándar para un borde con degradado.

El prefijo ya no hace falta. -webkit-mask fue necesario durante años y hoy la propiedad sin prefijo funciona en los tres motores. Si mantienes código antiguo verás las dos versiones; en código nuevo, sin prefijo.

clip-path corta los eventos y mask no, y esa asimetría se usa a propósito

Un elemento con clip-path no recibe eventos de puntero fuera de la región recortada. Un elemento con mask los recibe en todas partes, incluida la zona con valor de máscara cero.

No es un descuido de la especificación: son dos operaciones conceptualmente distintas. El recorte modifica la geometría del elemento, y la geometría es lo que determina el hit testing. La máscara modifica su apariencia en la fase de composición, que ocurre después de que la geometría esté fijada.

Las dos consecuencias prácticas van en direcciones opuestas y las dos se explotan.

A favor: un elemento enmascarado que sigue capturando eventos es exactamente lo que quieres en una lista con desvanecido en los bordes. El texto difuminado sigue siendo seleccionable y los enlaces del borde siguen siendo clicables, que es lo correcto.

En contra: un elemento invisible por máscara sigue bloqueando los clics de lo que hay debajo. Un velo enmascarado a cero, usado como capa de transición, se convierte en una trampa invisible que hace que la interfaz deje de responder. El síntoma es «los botones no funcionan y no hay nada encima». La causa es una capa con máscara y sin pointer-events: none.

La regla operativa: toda capa cuya visibilidad se controla con máscara lleva pointer-events: none cuando está oculta. Y si lo que necesitas es que el corte sea real a todos los efectos, la herramienta es clip-path, no mask.

⚔️ Reto práctico

Monta una lista con scroll cuyo contenido se desvanece arriba y abajo, y verifica dos cosas: que el texto difuminado se puede seleccionar y que un enlace en la zona difuminada sigue siendo clicable. Después haz lo mismo con clip-path en lugar de máscara y observa que el texto se corta en seco y que los enlaces cortados dejan de responder. Con eso tienes la asimetría medida, no leída.