wandres.dev
ESTILAR SVG · Presentación, CSS y herencia

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.

⏱ 15 min

En HTML casi ninguna propiedad visual se hereda: background, border, padding se aplican a un elemento y ahí se quedan. En SVG ocurre lo contrario con la pintura: fill, stroke y toda su familia bajan por el árbol. Esa asimetría no es un accidente, es lo que permite estilar una ilustración de doscientos nodos con una sola declaración, y también lo que hace que opacity se comporte de una manera que sorprende a todo el mundo la primera vez.

🎯 Al terminar esta lección sabrás
  • Enumerar qué propiedades de pintura se heredan y cuáles no.
  • Usar g como contenedor de estilo y saber qué gana con ello frente a repetir atributos.
  • Explicar la diferencia entre fill-opacity en cada hijo y opacity en el grupo.
  • Predecir el resultado de anidar grupos con opacidad.

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.

<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.

<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.

<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.

Las opacidades de grupo se multiplican, y eso hace desaparecer capas enteras

opacity no se hereda, pero se compone. Un grupo al 50 por ciento dentro de otro al 50 por ciento produce un 25 por ciento efectivo, porque cada nivel se rasteriza y se compone contra el de arriba.

Eso convierte el anidamiento en una trampa práctica. En un gráfico donde la capa de datos está atenuada al 40 por ciento durante una transición, y dentro hay una serie que ya venía al 30 por ciento porque estaba deseleccionada, el resultado es un 12 por ciento: invisible sobre cualquier fondo. El desarrollador que escribió el 30 y el que escribió el 40 no se conocen y ninguno de los dos hizo nada mal.

La disciplina que lo evita: una sola capa del árbol es dueña de la opacidad. Si el estado de selección se expresa con opacidad, que sea siempre en el mismo nivel, y que las transiciones globales usen otro mecanismo (un rectángulo de velo encima, o fill hacia un gris). Y cuando tengas que depurarlo, la propiedad computada de cada nodo no te lo va a decir: cada uno declara su valor correcto. Lo que hay que mirar es la cadena de ancestros, multiplicando.

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:

<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>
.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.

⚔️ Reto práctico

Dibuja tres círculos solapados en el mismo grupo y consigue que la intersección de los tres sea visiblemente más oscura que la de dos, usando solo herencia y sin repetir ninguna declaración en los hijos. Después consigue lo contrario: que el conjunto se atenúe sin que ninguna intersección se note. Anota qué propiedad has usado en cada caso y en qué nivel del árbol.