Saltar al contenido

La herencia de las propiedades de pintura

Por qué fill y stroke se heredan y opacity no, cómo un grupo se convierte en un contenedor de estilo, y la diferencia entre apilar opacidades y aplicarla al grupo entero.

Qué baja y qué no

La regla es sencilla de enunciar: las propiedades que describen cómo se pinta se heredan; las que describen qué se hace con el resultado, no.

Se heredan: fill, fill-opacity, fill-rule, stroke y todas las stroke-*, paint-order, marker-start, marker-mid, marker-end, color, color-interpolation-filters, clip-rule, shape-rendering, text-anchor, dominant-baseline, visibility, cursor, pointer-events y toda la familia font-*.

No se heredan: opacity, filter, clip-path, mask, mix-blend-mode, isolation, transform, vector-effect, display, overflow.

La lógica detrás del corte es la de la composición. fill es una instrucción para el rasterizador de cada forma, y tiene sentido que la comparta un subárbol entero. opacity es una operación sobre el resultado ya rasterizado: heredarla significaría aplicarla varias veces, una por nivel, y multiplicar opacidades sin que nadie lo haya pedido.

El grupo como contenedor de estilo

g no dibuja nada. Su función es agrupar para transformar y para estilar, y en el segundo papel es donde se gana el sueldo.

HTML
<svg viewBox="0 0 200 100" width="400">  <g fill="none" stroke="#89b4fa" stroke-width="3"     stroke-linecap="round" stroke-linejoin="round">    <path d="M 20 80 L 50 30 L 80 60 L 110 20" />    <path d="M 20 20 H 110" />    <circle cx="150" cy="50" r="25" />  </g></svg>

Cuatro declaraciones para tres formas. Sin el grupo serían doce atributos repetidos, y cambiar el grosor sería tocar tres sitios. Con quinientas formas, la diferencia deja de ser estética y pasa a ser de peso: un icono complejo con el estilo en el grupo pesa la mitad que el mismo icono con el estilo repetido en cada ruta, y es exactamente lo que hace la opción de exportación limpia de las herramientas de diseño cuando se lo pides bien.

Hay una segunda ventaja menos obvia. Un valor heredado desde el grupo se puede sobrescribir en un hijo concreto sin luchar con la especificidad, porque la herencia es el escalón más débil de todos: pierde contra cualquier declaración explícita, venga de donde venga.

HTML
<g fill="#585b70">  <circle cx="30" cy="50" r="20" />  <circle cx="80" cy="50" r="20" fill="#a6e3a1" />  <!-- este manda --></g>

Y la tercera: el grupo es un punto de anclaje para CSS. g.grafico { stroke: var(--linea) } estiliza el subárbol entero sin que ninguna forma tenga que llevar clase.

opacity frente a fill-opacity

Aquí está la diferencia que produce el desconcierto. Las dos parecen hacer lo mismo y hacen cosas distintas.

fill-opacity afecta solo al relleno de cada forma, y se hereda. Si dos formas de un grupo se solapan y ambas tienen fill-opacity: 0.5, la zona común se ve más opaca, porque son dos capas semitransparentes superpuestas.

opacity sobre el grupo rasteriza el grupo entero como una unidad y luego aplica la transparencia al resultado. La zona de solape no se ve más opaca, porque se compuso a plena opacidad antes de aplicar el 50 por ciento.

HTML
<svg viewBox="0 0 260 100" width="520">  <!-- Opacidad por forma: el solape se acumula -->  <g fill="#89b4fa" fill-opacity="0.5">    <circle cx="45" cy="50" r="30" />    <circle cx="80" cy="50" r="30" />  </g>  <!-- Opacidad de grupo: el solape es uniforme -->  <g fill="#89b4fa" opacity="0.5">    <circle cx="175" cy="50" r="30" />    <circle cx="210" cy="50" r="30" />  </g></svg>

En el primero se ve claramente la lente de intersección más oscura. En el segundo el par de círculos parece una sola forma translúcida. Cuál quieres depende del caso: para un diagrama de Venn, el primero; para atenuar una capa entera de un gráfico, el segundo, siempre.

El coste no es el mismo. opacity menor que 1 sobre un grupo obliga al motor a crear un búfer temporal, rasterizar el subárbol ahí y componerlo después. Es exactamente el mismo mecanismo que dispara filter, mask y mix-blend-mode. En un grupo pequeño es irrelevante; en uno con cinco mil nodos que se anima cada fotograma, es la diferencia entre sesenta imágenes por segundo y quince.

Un caso concreto: tematizar un icono heredado

Con lo anterior en la mano, el patrón correcto para un icono que se adapta al contexto se escribe solo:

HTML
<svg class="icono" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">  <g fill="none" stroke="currentColor" stroke-width="2"     stroke-linecap="round" stroke-linejoin="round">    <path d="M3 12h18" />    <path d="M12 3v18" />  </g></svg>
CSS
.icono { color: inherit; }.boton-peligro .icono { color: #f38ba8; }

Ni una clase en las rutas, ni un fill que pelee con nada, y el color viaja por la única propiedad que estaba pensada para viajar. currentColor merece su propia explicación y la tiene en currentColor y el icono que se adapta.