use y su árbol de sombra
Qué clona exactamente use, por qué los selectores no llegan al contenido instanciado, cómo funcionan x e y, y las tres cosas que sí cruzan la frontera.
use es el mecanismo de reutilización de SVG y también la fuente de la queja más repetida del formato: «no puedo estilar el contenido del sprite». La queja es correcta y la razón es precisa: use no copia nodos al documento, crea un árbol de sombra. Saber exactamente qué cruza esa frontera y qué no convierte una limitación desconcertante en un contrato claro con el que se puede diseñar.
- Describir qué construye
useen el DOM y en qué se diferencia de una copia. - Explicar por qué un selector del documento no alcanza el contenido instanciado.
- Enumerar los tres canales que sí atraviesan la frontera de sombra.
- Usar
x,y,widthyheightsabiendo cuándo tienen efecto y cuándo no.
Qué construye use
use toma una referencia con href y construye, en un árbol de sombra colgado del propio use, una copia profunda del elemento referenciado y de todos sus descendientes.
<svg viewBox="0 0 120 60" width="240">
<defs>
<circle id="p" cx="0" cy="0" r="15" fill="#89b4fa" />
</defs>
<use href="#p" x="30" y="30" />
<use href="#p" x="90" y="30" />
</svg>
En el inspector verás dos elementos use, cada uno con un #shadow-root dentro que contiene un circle. No verás dos circle en el documento: los círculos no son hijos del svg, son contenido de sombra.
Las consecuencias de esa decisión de diseño, en orden de importancia práctica:
El contenido no es seleccionable desde CSS. Una regla use circle { fill: red } no encuentra nada. El combinador de descendiente no cruza la frontera de sombra, y no existe un selector que lo haga desde fuera.
El contenido no es accesible desde querySelector. svg.querySelectorAll('circle') devuelve cero elementos. Para llegar hay que atravesar useEl.shadowRoot, y en SVG ese shadowRoot es cerrado: la propiedad no está expuesta. El contenido instanciado es, a efectos de JavaScript, invisible.
Los identificadores no se duplican en el documento. Si el elemento original tiene descendientes con id, esos identificadores viven dentro de cada árbol de sombra sin colisionar entre sí ni con el documento. Es la única propiedad buena de esta arquitectura.
Los eventos se disparan sobre el use. Un clic en el círculo instanciado tiene como target el elemento use, no el círculo, porque el evento se reajusta al cruzar la frontera. Eso simplifica bastante el manejo: siempre sabes qué instancia se ha tocado.
Por qué se diseñó así y no como una copia
La alternativa evidente habría sido que use copiara los nodos al documento ligero. SVG 1.1 lo describía justamente así, con un lenguaje de «árbol generado» que los motores implementaron de formas distintas. SVG 2 lo formalizó como árbol de sombra, y la razón es la duplicación de identificadores.
Si use copiara nodos, instanciar cinco veces un símbolo que contiene <path id="cuerpo"> produciría cinco elementos con el mismo id en el documento. Eso rompe getElementById, rompe url(#cuerpo) y rompe aria-labelledby. Con el árbol de sombra, cada instancia tiene su propio espacio de nombres y el problema desaparece.
El precio es la imposibilidad de estilar por selector. Es un intercambio, y visto desde el problema que resuelve, es el correcto.
Los tres canales que cruzan
No todo se detiene en la frontera. Tres cosas la atraviesan, y con ellas se construye todo lo que se puede construir.
Uno: las propiedades heredadas. El árbol de sombra hereda del elemento use. fill, stroke, stroke-width, color, font-size y toda la familia de la pintura bajan. De ahí que currentColor funcione en un sprite.
<use href="#p" x="30" y="30" style="color:#f38ba8" />
Dos: las custom properties. Son propiedades heredadas, así que cruzan. Es el canal para llevar más de un color, y el patrón completo está en custom properties dentro de SVG.
Tres: los selectores que apuntan al propio use. El use sí es un elemento del documento con sus clases y sus atributos. use.grande { width: 48px } funciona perfectamente.
Lo que no cruza: cualquier selector de descendiente, cualquier propiedad no heredada, y ::part (que requiere que el componente exponga partes, y SVG no lo hace).
La herencia es el escalón más débil de la cascada. Eso significa que si el elemento original declara un valor, la herencia del use no lo toca:
<symbol id="i">
<path fill="#585b70" d="..." /> <!-- declarado -->
</symbol>
<use href="#i" style="color:#f38ba8" /> <!-- no hace nada -->El icono sale gris pase lo que pase, porque fill="#585b70" es una declaración en el clon y le gana a cualquier valor heredado. La gente lo intenta con style en línea, con !important sobre use, con selectores más específicos, y nada funciona: el problema no es de especificidad, es de destinatario. La declaración está en un elemento al que no puedes apuntar, y le gana a la herencia por definición.
La única solución es cambiar el original: el símbolo tiene que declarar fill="currentColor" o fill="var(--ico, #585b70)" en lugar de un color fijo. Si el sprite viene de una herramienta y no puedes tocarlo, el arreglo es un paso de procesado que sustituya los colores por currentColor antes de servirlo. No hay ningún truco de CSS que lo resuelva desde fuera, y merece la pena saberlo antes de perder una tarde probando combinaciones de especificidad.
x, y, width y height
use tiene cuatro atributos geométricos y sus reglas no son evidentes.
x e y se comportan como una traslación adicional aplicada al contenido instanciado, después de cualquier transform del propio use. Por eso el patrón habitual es definir la forma centrada en el origen y colocar las instancias con x e y:
<defs><circle id="p" cx="0" cy="0" r="10" fill="#a6e3a1" /></defs>
<use href="#p" x="20" y="20" />
<use href="#p" x="60" y="20" />
width y height solo tienen efecto si el elemento referenciado es un svg o un symbol. Sobre un path, un g o un circle se ignoran por completo. Esa es una de las razones para usar symbol en un sprite: sin él, no puedes redimensionar la instancia por atributos.
Y hay una restricción que se olvida: use no puede referenciarse a sí mismo ni crear un ciclo. Un use que apunta a un ancestro suyo produce una referencia circular, y el motor la detecta y no renderiza nada. Ocurre en sprites generados automáticamente donde dos símbolos se referencian mutuamente.
Cuándo use no es la respuesta
Tres situaciones donde conviene no usarlo.
Cuando cada instancia necesita geometría distinta. use clona; no puede cambiar la d de una instancia. Si las instancias difieren en más que posición, tamaño y colores heredados, hay que generar marcado distinto.
Cuando necesitas seleccionar dentro desde JavaScript. Si tu código tiene que encontrar y modificar una parte concreta del contenido instanciado, use es la herramienta equivocada. Genera el marcado en línea, aunque se repita.
Cuando el número de instancias es enorme y cambian a menudo. Cada use construye y mantiene un árbol de sombra. Con diez mil instancias animadas, el coste de mantener diez mil árboles de sombra supera al de tener el marcado repetido. Es un caso raro, pero existe en visualización de datos.
Monta un sprite con tres símbolos y comprueba las cuatro afirmaciones de este artículo: que svg.querySelectorAll('path') no encuentra el contenido instanciado; que el target de un clic es el use; que una custom property definida en el use llega al fill de dentro; y que un fill declarado en el símbolo bloquea esa custom property. Con las cuatro comprobadas, el modelo mental queda cerrado.