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

mask con imágenes, degradados y composición

Las ocho propiedades de máscara y sus valores iniciales sorprendentes, la diferencia entre modo alfa y modo luminancia, y las cuatro operaciones de mask-composite.

⏱ 20 min

mask es la familia de propiedades que más se parece a background: mismas capas, mismos nombres, misma sintaxis de posición y tamaño. Esa simetría es deliberada y ayuda, salvo en dos puntos donde los valores iniciales no son los que esperas y donde el modo de interpretación cambia según de dónde venga la máscara. Con esas dos cosas claras, se abre la composición por capas, que es lo que de verdad tiene aquí valor.

🎯 Al terminar esta lección sabrás
  • Escribir las ocho propiedades de máscara y sus abreviadas.
  • Explicar la diferencia entre alpha, luminance y match-source.
  • Componer varias capas de máscara con las cuatro operaciones de conjunto.
  • Construir el borde de degradado con esquinas redondeadas.

La familia y sus valores iniciales

Propiedad Valor inicial Equivalente en fondos
mask-image none background-image
mask-position center background-position
mask-size auto background-size
mask-repeat repeat background-repeat
mask-origin border-box background-origin
mask-clip border-box background-clip
mask-mode match-source no tiene
mask-composite add no tiene

Dos de esos valores iniciales muerden. mask-repeat vale repeat, igual que en fondos, con lo que un degradado de máscara sin mask-size explícito y con no-repeat puede acabar repitiéndose si le das un tamaño. Y mask-position vale center, no 0% 0% como en fondos, lo cual es una asimetría que sorprende al portar código de una familia a la otra.

mask-origin vale border-box, otra asimetría: en fondos el origen es padding-box. Aquí tiene sentido, porque una máscara suele querer cubrir el elemento entero, borde incluido.

La abreviada acepta todo, con la barra separando posición de tamaño y las palabras de caja en el orden origen y luego recorte:

.a {
  mask: url(forma.svg) center / contain no-repeat;
}
.b {
  mask: linear-gradient(#000, transparent) 0 0 / 100% 100% no-repeat border-box;
}

Alfa, luminancia y match-source

Una máscara puede interpretarse de dos formas. En modo alfa, lo que importa es el canal alfa de la imagen de máscara: opaco enmascara al 100%, transparente al 0%, y el color se ignora por completo. En modo luminancia, lo que importa es el brillo: blanco enmascara al 100%, negro al 0%, y el alfa se multiplica encima.

mask-mode: match-source es el valor inicial y decide según el origen: para cualquier valor de tipo imagen, incluidos los degradados, el modo es alfa; para una referencia a un elemento <mask> de SVG, luminancia.

Eso explica un montón de confusión. Un degradado de blanco a negro no enmascara nada: los dos extremos son opacos, así que en modo alfa la máscara vale 1 en todas partes. El degradado que enmascara es de un color opaco a transparente:

/* No hace nada: los dos colores son opacos. */
.mal { mask-image: linear-gradient(#fff, #000); }

/* Correcto en modo alfa. */
.bien { mask-image: linear-gradient(#000, transparent); }

/* Correcto si fuerzas luminancia. */
.tambien { mask-image: linear-gradient(#fff, #000); mask-mode: luminance; }

Cuando el degradado se usa en modo alfa, el color es irrelevante: #000, #fff o red producen exactamente el mismo resultado mientras el alfa sea el mismo. Por convención se escribe negro, y da igual.

Forzar luminance tiene un uso concreto: reutilizar como máscara una imagen en escala de grises que no tiene canal alfa, cosa habitual cuando la máscara viene de una textura o de un mapa generado.

Componer varias máscaras

Aquí está lo que hace de mask una herramienta y no un truco. Cada capa de máscara se combina con el resultado acumulado de las anteriores mediante mask-composite, que ofrece cuatro operaciones de conjuntos:

Valor Resultado
add Unión. Es el valor inicial
subtract La capa actual menos las anteriores
intersect Intersección
exclude Diferencia simétrica: en una o en otra, pero no en las dos

Con eso se construyen regiones que ninguna forma describe. El caso canónico es el anillo, y con él el borde de degradado que sigue las esquinas redondeadas, que es la asignatura pendiente de border-image:

.borde-degradado {
  position: relative;
  border-radius: 16px;
}

.borde-degradado::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 2px;
  background: linear-gradient(in oklch to bottom right, #89b4fa, #f38ba8);

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

  pointer-events: none;
}

Merece la pena leerlo despacio porque cada línea hace falta. El pseudoelemento cubre el elemento entero y lleva el degradado de fondo. Tiene dos píxeles de relleno, así que su caja de contenido es el rectángulo interior. La primera capa de máscara es un rectángulo opaco recortado a content-box, es decir, el interior. La segunda es un rectángulo opaco sin recorte, es decir, todo. exclude deja lo que está en una y no en la otra: el anillo de dos píxeles entre ambas. Y como el pseudoelemento hereda el radio, el anillo sigue la curva.

linear-gradient(#000 0 0) es el idioma para “una imagen de un color plano”: un degradado con las dos paradas en la misma posición. No hay una función solid-color() en CSS, así que este es el atajo estándar.

⚠️
La sintaxis prefijada de composite usa otras palabras

Las versiones antiguas de WebKit implementaron -webkit-mask-composite con las palabras de Porter-Duff: source-over, source-in, source-out y xor. El equivalente de exclude es xor. Si necesitas cubrir esas versiones, declara las dos, y ten en cuenta que el orden de las capas se interpreta al revés en la sintaxis prefijada.

Otros patrones de una línea

Desvanecer el final de un contenedor con desplazamiento:

.desplazable {
  mask-image: linear-gradient(to bottom, #000 calc(100% - 48px), transparent);
}

Recortar una imagen con una forma suave sin tocar el archivo:

.retrato {
  mask-image: radial-gradient(circle closest-side, #000 70%, transparent);
}

Un icono monocromo coloreable desde CSS, que es la alternativa moderna a las fuentes de iconos:

.icono {
  display: inline-block;
  inline-size: 1.25em;
  block-size: 1.25em;
  background: currentColor;
  mask: url(iconos/flecha.svg) center / contain no-repeat;
}

El SVG aporta la forma y el color lo pone currentColor, así que el icono sigue al texto en cualquier estado y en cualquier tema sin duplicar archivos.

Una máscara es un canal alfa, y por tanto compone con todo lo demás que multiplique alfa

La forma productiva de pensar en mask no es “recortar con una imagen” sino esta: la máscara es un multiplicador del canal alfa del elemento, y no es el único que hay. opacity es otro. El alfa del propio contenido es otro. El alfa que aporte un filter es otro. Todos se multiplican, y entender que se multiplican en un orden concreto resuelve efectos que de otro modo parecen imposibles y explica resultados que de otro modo parecen fallos. El orden es: primero el elemento y su subárbol se pintan con sus alfas propios, después se aplican los filtros, después la máscara, y por último opacity al componer el resultado sobre lo que hay detrás. De ahí sale que opacity: 0.5 sobre un elemento enmascarado produce medio alfa de la región enmascarada, no una máscara al 50%, y que la diferencia se ve en cuanto hay solapamiento interno: un elemento con hijos que se superponen y opacity: 0.5 se ve uniformemente traslúcido, mientras que los mismos hijos con opacity: 0.5 cada uno se ven acumulados donde se solapan. Y sale también el patrón que muy poca gente usa: como la máscara se aplica antes de opacity, se pueden encadenar dos niveles de transparencia independientes, uno espacial y otro global, y animar solo el segundo. Un desvanecimiento de borde que es fijo y una entrada que es animada no compiten por la misma propiedad: la máscara define dónde y opacity define cuánto, y las dos conviven sin que ninguna tenga que recalcularse. Es la misma separación que hay entre forma y color en cualquier sistema gráfico, y es la razón por la que la máscara es una propiedad de composición y no un simple recorte con degradado.

⚔️ Compón con máscaras
  1. Enmascara con linear-gradient(#fff, #000) y comprueba que no hace nada. Arréglalo de las dos formas.
  2. Monta el borde de degradado con mask-composite: exclude y cámbiale el radio.
  3. Convierte un SVG monocromo en un icono coloreable con currentColor.
  4. Prueba las cuatro operaciones de composición con dos círculos superpuestos.
  5. Combina una máscara de desvanecimiento fija con una animación de opacity y verifica que no interfieren.