wandres.dev
GRADIENTES Y PATRONES · Rellenos complejos

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.

⏱ 16 min

Gradientes, patrones, filtros, máscaras, recortes y marcadores se referencian todos igual: url(#algo). Y ese #algo se resuelve contra el documento entero, sin ámbito, sin encapsulación y sin aviso cuando hay dos. En una página con un componente es irrelevante; en una aplicación donde treinta componentes inyectan SVG generados por la misma herramienta, es el fallo que hace que un icono se pinte con el gradiente de otro y que nadie sepa de dónde sale.

🎯 Al terminar esta lección sabrás
  • Explicar cómo se resuelve una referencia por fragmento y contra qué ámbito.
  • Reproducir la colisión de identificadores y reconocer su síntoma característico.
  • Aplicar las tres estrategias de aislamiento y elegir según el contexto.
  • Anticipar los dos casos históricos donde url(#id) se resolvía mal.

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.

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

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

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.

El identificador que colisiona más a menudo no es tuyo, es el de la herramienta de diseño

Figma genera identificadores de la forma clip0_1_2, paint0_linear_34_567 y filter0_d_12_3. Los números salen del identificador interno del nodo y del fichero de origen, no de un contador global. Eso significa que dos exportaciones del mismo fichero de Figma producen identificadores idénticos con frecuencia, y que dos iconos exportados de la misma página son firmes candidatos a colisionar.

El más peligroso es clip0_.... Figma envuelve el contenido en un clipPath cuando el marco recorta, y ese recorte se referencia con clip-path="url(#clip0_1_2)". Si dos iconos comparten ese identificador, el segundo se recorta con el rectángulo del primero. Y como los dos rectángulos suelen ser del mismo tamaño, el fallo es invisible mientras los dos iconos midan lo mismo. El día que alguien añade un icono de otra rejilla, ese icono aparece cortado a la mitad y no hay nada en su marcado que lo explique.

Illustrator tiene una variante distinta y peor: la opción de exportar con «CSS interno» produce un bloque <style> con clases .st0, .st1, .st2. Esas clases no son identificadores, son selectores globales de CSS, así que no solo colisionan entre iconos: se aplican a cualquier elemento de la página que tenga esa clase. Un icono de Illustrator inyectado en línea puede repintar medio documento. La opción de exportación correcta es «atributos de presentación», y esa decisión se toma en la herramienta, antes de que el fichero llegue al repositorio.

Regla operativa: todo SVG que entre en el proyecto pasa por un paso de prefijado, sin excepciones, aunque hoy no tenga ninguna referencia. El día que alguien reexporte el icono con un recorte, el paso ya está ahí.

⚔️ Reto práctico

Reproduce la colisión: inyecta dos veces el mismo componente con un gradiente de identificador fijo, cada uno con colores distintos, y confirma que los dos se pintan igual. Después desmonta el primero y observa cómo cambia el segundo. Por último, añade un generador de identificadores único y comprueba que el problema desaparece. Guarda el caso de prueba: sirve para verificar que el paso de prefijado del build sigue haciendo su trabajo.