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