Custom properties dentro de SVG
Las variables CSS como único canal que atraviesa el árbol de sombra de use, el patrón de icono multicolor tematizable, y la trampa de los valores de reserva.
Las custom properties resuelven en SVG un problema que no tiene otra solución: llevar más de un color a un contenido que no puedes seleccionar. Se heredan como cualquier propiedad, cruzan las fronteras de sombra que los selectores no cruzan, y admiten un valor de reserva que hace que el fichero funcione por sí solo. Eso las convierte en el mecanismo estándar del icono multicolor tematizable, y en la única respuesta correcta a «cómo cambio dos colores de un sprite».
- Declarar y consumir custom properties dentro de un documento SVG.
- Diseñar un icono multicolor con valores de reserva que funcione sin CSS externo.
- Explicar por qué las variables cruzan el árbol de sombra de
usey los selectores no. - Registrar una custom property con
@propertypara poder animarla.
Se comportan igual que en HTML
No hay nada especial en cómo funcionan dentro de un SVG: se declaran en cualquier elemento, se heredan a los descendientes y se consumen con var(). Lo distinto es lo que permiten hacer.
<svg viewBox="0 0 24 24" width="48" height="48" class="icono-doc">
<rect x="4" y="2" width="14" height="20" rx="2"
fill="var(--relleno, #45475a)" />
<path d="M8 8h6M8 12h6M8 16h4"
stroke="var(--lineas, #cdd6f4)" stroke-width="1.5" stroke-linecap="round" />
</svg>
.icono-doc { --relleno: #1e1e2e; --lineas: #89b4fa; }
[data-tema="claro"] .icono-doc { --relleno: #eff1f5; --lineas: #1e66f5; }
Dos observaciones que importan.
La primera: el valor de reserva del segundo argumento de var() no es un detalle cosmético. Hace que el fichero se vea correctamente abierto directamente en el navegador, incrustado en un correo, o en cualquier contexto donde el CSS del sistema de diseño no llegue. Un icono cuyo fill es var(--relleno) sin reserva se pinta con el valor inicial de fill, que es negro, y en un fondo oscuro desaparece.
La segunda: las custom properties se pueden usar en cualquier atributo de presentación, porque son propiedades. No se pueden usar en viewBox, en d ni en points, porque esos no lo son.
Por qué cruzan el árbol de sombra
Este es el punto que justifica el nivel entero. Cuando use instancia un symbol, el contenido clonado vive en un árbol de sombra. Los selectores del documento no llegan ahí: #sprite .fondo { fill: red } no encuentra nada, porque .fondo está dentro de la sombra y el selector se evalúa en el documento ligero.
Lo que sí cruza es la herencia. El árbol de sombra hereda del elemento use, exactamente igual que un elemento HTML hereda de su padre. Y como las custom properties son propiedades heredadas, cruzan.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="i-carpeta" viewBox="0 0 24 24">
<path fill="var(--c-cuerpo, #585b70)" d="M2 6h7l2 2h11v12H2z" />
<path fill="var(--c-pestana, #7f849c)" d="M2 6h7l2 2H2z" />
</symbol>
</svg>
<svg class="ico ico-aviso" width="32" height="32" aria-hidden="true">
<use href="#i-carpeta" />
</svg>
<svg class="ico ico-ok" width="32" height="32" aria-hidden="true">
<use href="#i-carpeta" />
</svg>
.ico-aviso { --c-cuerpo: #f9e2af; --c-pestana: #fab387; }
.ico-ok { --c-cuerpo: #a6e3a1; --c-pestana: #94e2d5; }
Dos instancias del mismo símbolo, con paletas distintas, sin duplicar el marcado y sin un solo selector que apunte dentro de la sombra. Es el patrón, y no hay otro que funcione.
El comportamiento de var() cuando la variable no existe es más sutil de lo que parece, y produce un fallo desconcertante en sprites.
Si --c-cuerpo no está definida en ninguna parte, var(--c-cuerpo, #585b70) usa la reserva y todo va bien. Pero si está definida con un valor inválido para esa propiedad (por ejemplo, alguien puso --c-cuerpo: 12px porque la reutilizó para otra cosa en un ancestro), la reserva no se usa. La sustitución tiene éxito, produce fill: 12px, y eso es una declaración inválida en tiempo de cálculo. La regla del CSS para ese caso no es descartar la declaración: es tratar la propiedad como no heredada con su valor inicial si es heredable, o unset. En fill, cuyo valor inicial es negro, el resultado es un icono negro.
El síntoma: un icono de un sprite sale negro solo en una parte del sitio, la parte donde un componente ajeno declaró una variable con el mismo nombre para otro propósito. No hay error en consola, el valor computado dice negro y nadie escribió negro en ninguna parte.
Las dos defensas: prefija las variables del sistema de iconos (--ico-cuerpo, no --cuerpo), y si el proyecto es grande, regístralas con @property y un syntax de tipo color. Registrada, una variable con valor inválido cae a su initial-value en lugar de contaminar la propiedad, y el fallo deja de propagarse.
Registrar con @property
@property declara el tipo de una custom property, su valor inicial y si se hereda. Dos beneficios concretos en SVG.
Uno: el valor inválido no rompe nada. Con syntax: "<color>", cualquier valor que no sea un color se descarta en el parseo y la propiedad conserva su initial-value.
Dos: la variable se puede animar e interpolar. Una custom property sin registrar es una cadena de texto: el navegador no sabe que #89b4fa y #f38ba8 son colores, así que una transición sobre ella salta de golpe a la mitad. Registrada como color, se interpola.
@property --ico-cuerpo {
syntax: "<color>";
inherits: true;
initial-value: #585b70;
}
.ico { transition: --ico-cuerpo 200ms ease; }
.ico:hover { --ico-cuerpo: #89b4fa; }
Ese transition sobre una variable es lo que permite animar el color de un icono dentro de un árbol de sombra, donde no puedes poner una transición en el elemento que realmente se pinta porque no puedes seleccionarlo. La transición vive en el use, la variable viaja por herencia, y el fill de dentro se recalcula en cada fotograma.
Un syntax de tipo <length> o <number> sirve igual para animar cosas que no son colores: el stroke-width de un icono de línea, la stroke-dashoffset de un trazo progresivo, o la opacidad de una capa.
Los límites
Tres cosas que las custom properties no arreglan.
No cruzan la frontera del documento. Un SVG cargado con img o como background-image no ve las variables de la página, por la misma razón que no ve color. Solo funcionan con SVG en línea y con use.
No se pueden usar en atributos que no son propiedades. viewBox="0 0 var(--w) 24" no funciona. Nada geométrico de configuración funciona.
No sustituyen a una API de componente. Un icono con siete variables es un icono que nadie sabe usar. Dos o tres tonos con nombres semánticos (--ico-base, --ico-acento) es un contrato manejable; siete es un mecano.
Y un apunte de nomenclatura que ahorra mucho: nombra por papel, no por color. --ico-acento sobrevive a un rediseño; --ico-azul obliga a un renombrado global el día que el acento pase a ser verde.
Convierte un sprite de cinco iconos multicolor a un sistema con tres variables tematizables, registradas con @property como colores. Comprueba tres cosas: que el sprite se ve correctamente al abrir el fichero SVG directamente en el navegador (gracias a las reservas), que cada instancia puede tener su paleta, y que una transición sobre una de las variables interpola suavemente en lugar de saltar.