currentColor y el icono que se adapta
Cómo funciona currentColor por dentro, por qué deja de funcionar en un SVG servido como imagen, y el patrón completo de un icono que hereda el color del texto.
currentColor es la palabra clave que convierte un fichero SVG en un componente reutilizable: un icono que toma el color del texto que lo rodea, sin clases, sin variantes y sin JavaScript. También es la palabra clave que deja de funcionar en silencio en cuanto ese mismo fichero se sirve con una etiqueta de imagen, y esa frontera es la que separa a quien entiende el mecanismo de quien lo copia.
- Explicar qué computa
currentColory en qué momento de la cascada. - Escribir un icono que hereda el color del contexto sin ninguna regla adicional.
- Identificar los tres modos de incrustar un SVG y decir en cuáles funciona
currentColor. - Aplicar el mismo mecanismo a un icono de dos tonos.
Qué es exactamente
currentColor no es un color: es una referencia al valor computado de la propiedad color del mismo elemento. Cuando escribes fill: currentColor en un path, el valor usado de fill es lo que valga color en ese path. Y como color se hereda, lo que valga en su ancestro más cercano que la declare.
El detalle que hace que todo encaje: en SVG, color no pinta nada por sí sola. No hay texto que colorear en un path. Su único papel es servir de canal de transporte, y por eso es la propiedad perfecta para el trabajo: nadie más la está usando.
<button class="boton">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
<path fill="currentColor" d="M12 2 15 9l7 .6-5.3 4.6 1.6 6.8L12 17.3 5.7 21l1.6-6.8L2 9.6 9 9z" />
</svg>
Destacar
</button>
.boton { color: #cdd6f4; }
.boton:hover { color: #f9e2af; }
El icono cambia de color en el hover sin que ninguna regla lo mencione. Es exactamente lo mismo que hace el texto, y por la misma vía.
Un matiz sobre el orden: currentColor se resuelve en el valor computado, no en el usado. Eso significa que si color cambia por una transición, fill transiciona con él sin declararlo, porque el navegador interpola el color computado. Es una de las pocas cosas que salen gratis.
Dónde funciona y dónde no
Hay tres maneras de meter un SVG en una página y currentColor solo funciona en dos.
SVG en línea. El marcado forma parte del documento, los nodos son nodos del DOM, la herencia funciona igual que con cualquier elemento HTML. Funciona.
use referenciando un símbolo del mismo documento. El contenido se clona en un árbol de sombra que hereda del elemento use, y color cruza la frontera porque la herencia cruza los árboles de sombra. Funciona.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="i-estrella" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2 15 9l7 .6-5.3 4.6 1.6 6.8L12 17.3 5.7 21l1.6-6.8L2 9.6 9 9z" />
</symbol>
</svg>
<span style="color:#f38ba8">
<svg width="24" height="24" aria-hidden="true"><use href="#i-estrella" /></svg>
</span>
img, background-image o un object. El SVG se carga como un documento independiente. Tiene su propio árbol, su propia cascada y su propia raíz. No hay ninguna relación de herencia con la página, así que color en el documento anfitrión no llega. currentColor se resuelve contra el valor inicial de color dentro del SVG, que es el negro por omisión, y el icono sale negro pase lo que pase. No funciona.
Esa frontera no es un bug ni una limitación caprichosa: es la misma que impide que la página lea el contenido del SVG, que el SVG ejecute scripts en ese modo, y que cargue recursos externos. Es una frontera de seguridad, y viene con todas las consecuencias.
Cuando alguien descubre que currentColor no funciona en background-image, la solución que circula es codificar el SVG como data URI e inyectar el color con una custom property:
.icono {
--c: %23f38ba8;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23f38ba8' d='...'/></svg>");
}Y para hacerlo variable, se concatena la variable dentro de la cadena. Funciona, y es una trampa por cuatro motivos. Primero: la almohadilla del color hay que escaparla como %23, así que el valor no es un color CSS sino una cadena, y no se puede interpolar en una transición. Segundo: la data URI no se cachea de forma independiente, así que el mismo icono repetido en cincuenta reglas se duplica cincuenta veces en el CSS. Tercero: no hay forma de dar al icono un nombre accesible, porque es un fondo. Y cuarto: cualquier cambio de color obliga a reconstruir la cadena entera, así que un tema oscuro duplica el CSS.
La alternativa correcta cuando de verdad necesitas el icono como fondo es mask-image en lugar de background-image: el SVG se usa como máscara de un background-color normal, y ese color sí es un color CSS con todo lo que eso implica: var(), currentColor, transiciones y temas.
.icono {
background-color: currentColor;
mask: url(/iconos/estrella.svg) center / contain no-repeat;
}El SVG puede ser un fichero externo cacheado, el color es una propiedad de verdad, y el icono es monocromo por construcción, que es lo que era de todas formas. La limitación es que solo sirve para iconos de un solo tono.
Un icono de dos tonos
currentColor da un canal. Cuando hacen falta dos, la combinación con custom properties da el resto, y es el patrón que usan las bibliotecas de iconos duotono:
<svg class="icono-duo" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<path class="fondo" d="M4 4h16v16H4z" />
<path class="frente" d="M8 12h8M12 8v8" />
</svg>
.icono-duo .fondo { fill: currentColor; opacity: .25; }
.icono-duo .frente { fill: none; stroke: currentColor; stroke-width: 2; }
/* Un segundo tono independiente cuando hace falta */
.icono-duo { --acento: #f9e2af; }
.icono-duo.acentuado .frente { stroke: var(--acento); }
La capa de fondo usa el mismo color con opacidad reducida, así que se adapta automáticamente a cualquier contexto. La de delante puede desviarse cuando se le pide. Y el icono sigue siendo un fichero neutro que funciona en cualquier tema.
Una advertencia sobre la opacidad como segundo tono: sobre un fondo oscuro, un currentColor claro al 25 por ciento da un gris azulado; sobre un fondo claro, el mismo cálculo da otro gris. Si tu diseño exige un valor exacto, la opacidad no sirve y necesitas una segunda custom property. Lo que la opacidad garantiza es la coherencia relativa, no el valor absoluto.
La regla operativa
Resumida en tres líneas para el sistema de diseño:
- Los iconos monocromos declaran
fill="currentColor"(ostroke="currentColor") en el propio marcado, como atributo. Así funcionan aunque no haya CSS. - El color se pone en el contenedor con la propiedad
color, nunca confilldesde una regla que apunte asvg. - Si el icono tiene que ir en un fondo CSS, usa
masky nobackground-image.
Con eso, el mismo fichero sirve para el tema claro, el oscuro, el estado de hover, el de error y el deshabilitado, sin variantes.
Coge un icono de tu proyecto y hazlo funcionar de las tres formas: en línea, por use desde un sprite, y como máscara CSS. Verifica en las tres que cambia de color al pasar el cursor por encima del botón que lo contiene, con una única regla que toque color. Anota qué has tenido que cambiar en el fichero para cada modo; si has tenido que cambiar algo, es que el fichero original tenía un fill fijo.