Atributos de presentación frente a CSS
Por qué un fill escrito en el elemento pierde contra cualquier regla de CSS, dónde encaja el atributo en la cascada, y la única forma fiable de blindar un color.
fill="red" parece un atributo y se comporta como una regla de CSS con especificidad cero. Esa frase resume todo el nivel y explica por qué un icono de tu sistema de diseño cambia de color en cuanto alguien escribe un selector universal en otra parte del proyecto. La cascada de SVG no es distinta de la de HTML: es la misma, con los atributos de presentación colocados en un sitio muy concreto y muy débil.
- Situar los atributos de presentación en el orden exacto de la cascada.
- Predecir el ganador entre un atributo, una regla de clase y un
styleen línea. - Explicar por qué esa colocación fue la decisión correcta pese a lo incómoda que resulta.
- Blindar un valor cuando de verdad no debe poder sobrescribirse.
Dónde entra el atributo en la cascada
Un atributo de presentación se traduce a una declaración de CSS y se inserta como si fuera una regla de autor situada al principio de la primera hoja de estilos de autor, con especificidad cero. No es un valor por defecto del elemento, no es una regla del navegador y no es un estilo en línea. Es una regla de autor debilísima.
De ahí sale el orden completo, de menos a más fuerte:
- Declaraciones del navegador.
- Declaraciones del usuario.
- Atributos de presentación.
- Cualquier regla de autor:
svg,.icono,#logo, todas. styleen línea.- Todo lo anterior marcado con
!important, en el orden inverso.
El resultado incómodo:
<style>
* { fill: rebeccapurple; }
</style>
<svg viewBox="0 0 24 24" width="48">
<circle cx="12" cy="12" r="10" fill="#a6e3a1" />
</svg>
El círculo sale morado. El selector universal tiene especificidad cero igual que el atributo, pero viene después en el orden de aparición, y a igualdad de especificidad y origen manda el último. No hay selector lo bastante débil para perder contra un atributo de presentación: incluso el más débil de todos empata y gana por orden.
Un style en línea sí gana al atributo, porque los estilos en línea van en su propio escalón:
<circle fill="#a6e3a1" style="fill: #f38ba8" r="10" cx="12" cy="12" />
Sale rojo. Y por si queda duda: el atributo de presentación no admite !important. La sintaxis fill="red !important" no es un valor de color válido y se descarta entero, dejando el elemento con el valor heredado.
Por qué se diseñó así
La colocación parece hostil hasta que se mira el problema que resolvía. SVG 1.0 se publicó en 2001, y su ambición era ser un formato de documento que se pudiera estilar con CSS igual que HTML. Pero los ficheros SVG los generaban herramientas de dibujo, que escriben la apariencia en el propio elemento porque no tienen un concepto de hoja de estilos.
Si los atributos hubieran tenido la fuerza de un estilo en línea, un fichero exportado de Illustrator habría sido inestilables: cada fill en cada ruta habría bloqueado cualquier intento de recolorearlo desde el documento anfitrión. Toda la práctica moderna de los iconos SVG (un fichero neutro que hereda el color del contexto) sería imposible.
Colocándolos al fondo de la cascada, la especificación consiguió las dos cosas a la vez: el fichero se ve correctamente por sí solo, y el documento que lo incrusta puede reescribirlo entero sin tocar el marcado. Es una decisión de diseño acertada cuyo coste es exactamente el que estás pagando cuando un selector suelto te cambia un icono.
Nadie escribe * { fill: purple }. Lo que sí escribe la gente, y está en media docena de bases de código, es algo de este estilo:
svg { fill: currentColor; }Una línea razonable en una hoja de utilidades, pensada para que los iconos monocromos hereden el color del texto. Y en el momento en que alguien mete en la página un SVG multicolor (un logotipo, una ilustración, un gráfico generado), todos sus rellenos declarados como atributo pierden contra esa regla y la ilustración entera sale del color del texto que la rodea.
El síntoma es inconfundible: una ilustración que en el fichero se ve bien y en la página sale monocroma. La causa está en una hoja de estilos que ni siquiera menciona ese componente. Y el arreglo correcto no es subir la especificidad del logotipo, sino acotar la regla original: svg.icono { fill: currentColor }, o mejor, invertirla y que el icono declare fill="currentColor" como atributo, que es lo que hace la biblioteca de iconos de cualquiera que haya pasado por esto. Escribir la utilidad como selector de tipo desnudo es sembrar un campo de minas cuyo alcance es todo el sitio.
Qué se puede escribir como atributo y qué no
No todo atributo de un elemento SVG es un atributo de presentación. La distinción es exacta: es atributo de presentación si existe una propiedad CSS con ese nombre. fill, stroke, opacity, clip-path, filter, text-anchor lo son. viewBox, points, d, preserveAspectRatio, gradientUnits, x1 de un gradiente y refX de un marcador no lo son: son atributos geométricos o de configuración, no participan en la cascada, y no se pueden escribir desde CSS.
Esa frontera es la fuente de una frustración recurrente. No puedes cambiar el viewBox desde una media query, ni el gradientUnits desde un tema, ni los points de un polígono desde un estado de hover. Para todo eso hay que tocar el atributo desde JavaScript o generar marcado distinto.
Los atributos de presentación se pueden escribir de las tres formas, y las tres producen la misma propiedad computada:
<circle fill="#89b4fa" />
<circle style="fill: #89b4fa" />
<circle class="azul" /> <!-- con .azul { fill: #89b4fa } -->
La lista completa de propiedades disponibles se trata en qué propiedades SVG viven en CSS.
Blindar un valor de verdad
Cuando un color no debe poder sobrescribirse (la marca en un logotipo, el rojo de un icono de error), hay tres niveles de defensa y conviene elegir el mínimo que funcione.
Nivel uno: style en línea. Gana a cualquier regla de autor sin !important. Es la opción sensata para un logotipo incrustado en un contexto que no controlas.
<path style="fill:#e0234e" d="..." />
Nivel dos: style en línea con !important. Gana a todo salvo a un !important de usuario. Reservado para cuando el contexto es hostil de verdad, como un widget que se inyecta en páginas de terceros.
Nivel tres: un shadow DOM. El estilo del documento no cruza la frontera del shadow salvo por herencia y por custom properties. Es la única barrera real, y es la razón por la que los componentes web de terceros no sufren este problema.
Y hay una defensa que no funciona y que todo el mundo prueba: subir la especificidad del selector propio. #logo path { fill: #e0234e } gana hoy, y pierde mañana contra #app #logo path. La escalada de especificidad no es una estrategia, es un aplazamiento.
Merece la pena decir en qué dirección va la práctica actual: en un sistema de diseño, lo correcto es no fijar el color en el SVG en absoluto. El icono declara fill="currentColor" y el color lo pone el contenedor con la propiedad color. Así el icono nunca pelea con la cascada: participa en ella por la vía de la herencia, que es la que estaba pensada para esto.
Monta una página con tres iconos: uno con fill como atributo, otro con fill en style en línea y otro con fill="currentColor". Añade después una hoja con svg { fill: hotpink } y observa cuáles cambian. Luego añade svg path { fill: teal } y vuelve a mirar. Anota el orden de la cascada que has deducido y compáralo con la lista de este artículo.