Saltar al contenido

Las referencias por id y el ámbito global

Por qué url(#id) es un espacio de nombres global compartido por todo el documento, qué pasa cuando dos componentes usan el mismo identificador, y las tres estrategias que lo resuelven.

Un solo espacio de nombres para todo

Los identificadores en HTML y en SVG comparten espacio: son el mismo id, resuelto por el mismo getElementById, en el mismo documento. Un <div id="g"> y un <linearGradient id="g"> colisionan.

La regla cuando hay duplicados es que gana el primero en orden de documento. No hay error, no hay aviso, y el segundo elemento simplemente nunca se referencia.

HTML
<!-- Componente A, inyectado primero --><svg viewBox="0 0 40 40" width="40">  <defs><linearGradient id="grad">    <stop offset="0" stop-color="#89b4fa" /><stop offset="1" stop-color="#cba6f7" />  </linearGradient></defs>  <circle cx="20" cy="20" r="18" fill="url(#grad)" /></svg><!-- Componente B, otro fichero, mismo id --><svg viewBox="0 0 40 40" width="40">  <defs><linearGradient id="grad">    <stop offset="0" stop-color="#a6e3a1" /><stop offset="1" stop-color="#f9e2af" />  </linearGradient></defs>  <circle cx="20" cy="20" r="18" fill="url(#grad)" /></svg>

Los dos círculos salen azul y morado. El segundo gradiente está en el documento, es válido, y no lo usa nadie.

El síntoma es característico y merece la pena memorizarlo: un elemento se pinta con el estilo de otro componente que aparece antes en la página. Y como depende del orden del documento, el fallo cambia según la ruta, según el orden de montaje de los componentes, y desaparece al aislar el componente en una página de pruebas. Es de los bugs que más tiempo consumen porque el componente afectado no tiene nada mal.

Peor todavía: si el componente A se desmonta y el gradiente sale del DOM, el círculo de B pasa a usar el suyo y cambia de color. Un componente que cambia de aspecto al desmontarse otro es un misterio absoluto si no conoces este mecanismo.

Las tres estrategias

Uno: prefijar en tiempo de compilación. El paso que procesa los SVG añade un prefijo derivado del nombre del fichero a todos los id y a todas las referencias. Es lo que hace el plugin prefixIds de SVGO, y es la solución correcta para un sistema de iconos.

HTML
<!-- despues del prefijado --><linearGradient id="icono-alerta_grad">...</linearGradient><circle fill="url(#icono-alerta_grad)" />

Ventaja: es determinista, no cuesta nada en tiempo de ejecución, y el marcado sigue siendo legible. Inconveniente: solo funciona si el SVG pasa por tu build.

Dos: generar identificadores únicos en tiempo de ejecución. Cada instancia del componente genera un sufijo y lo aplica a sus identificadores y referencias.

JS
let contador = 0;const uid = () => `g${(++contador).toString(36)}`;function grafico() {  const id = uid();  return `    <svg viewBox="0 0 100 60">      <defs><linearGradient id="${id}">        <stop offset="0" stop-color="#89b4fa"/><stop offset="1" stop-color="#f38ba8"/>      </linearGradient></defs>      <rect width="100" height="60" fill="url(#${id})"/>    </svg>`;}

Ventaja: funciona sin build y con contenido dinámico. Inconveniente: en renderizado de servidor con hidratación, el identificador generado en el servidor y el generado en el cliente tienen que coincidir o hay un desajuste. Los frameworks modernos ofrecen un generador de identificadores estables precisamente para esto; úsalo en lugar de un contador propio si tu framework lo tiene.

Tres: encapsular en un shadow DOM. Dentro de un árbol de sombra, los identificadores son locales y url(#id) se resuelve contra ese árbol. Dos componentes con el mismo identificador no se ven.

Ventaja: es la única encapsulación real, y la que no exige disciplina. Inconveniente: obliga a componentes web, y arrastra sus propias complicaciones de estilado.

Los dos casos históricos de resolución errónea

Merece la pena conocerlos porque siguen apareciendo en respuestas antiguas y confunden.

El caso del elemento base. Un <base href="..."> en el documento hace que las URL relativas se resuelvan contra esa base. Durante años, algunos motores aplicaban eso también a los fragmentos de url(#id), convirtiendo #grad en https://ejemplo.com/otra/ruta#grad, que no existe. El resultado era que todos los gradientes desaparecían en las páginas con base. Los motores actuales resuelven los fragmentos locales localmente, pero si trabajas con un sitio antiguo que usa base y ves gradientes ausentes, esa es la primera pista.

El caso de la aplicación de una sola página con rutas. El mismo mecanismo, pero disparado por el enrutador: si el código construye las referencias concatenando la ruta actual, cualquier navegación las rompe. La solución es la misma: usa siempre el fragmento desnudo, url(#id), nunca url(/ruta#id).

Y un tercer caso que sigue vigente: una referencia por fragmento no puede apuntar a otro documento para pintura. fill="url(sprite.svg#grad)" no funciona en ningún motor. Solo use puede cruzar ficheros.