defs y el contenido que no se pinta
Qué elementos de SVG nunca se renderizan por sí solos, por qué defs no es obligatorio y sí recomendable, y el orden de declaración en un documento con referencias.
SVG tiene una categoría de elementos que existen en el árbol, participan en la cascada y nunca se dibujan: son plantillas, no contenido. defs es el contenedor pensado para ellos, pero no es el mecanismo: el mecanismo es que cada uno de esos elementos declara por su cuenta que no se renderiza. Entender esa distinción evita el error de creer que defs oculta cosas, que es lo que casi todo el mundo cree.
- Enumerar los elementos que nunca se renderizan directamente y decir por qué.
- Explicar qué aporta
defscuando el contenido ya no se pinta de todos modos. - Colocar las referencias en el documento sin depender del orden de declaración.
- Distinguir un elemento no renderizado de uno oculto con
display: none.
La lista de los que no se pintan
Estos elementos no producen ningún píxel por el hecho de estar en el documento, estén donde estén:
defs, symbol, clipPath, mask, pattern, marker, filter, linearGradient, radialGradient, stop, metadata, title, desc, script, style y foreignObject cuando no tiene un contexto que lo soporte.
Cada uno lo declara por sí mismo. Un linearGradient colocado directamente como hijo del svg raíz, sin defs alrededor, no dibuja una franja de color en medio del lienzo: no dibuja nada, y se puede referenciar exactamente igual.
<!-- Esto funciona perfectamente, sin defs -->
<svg viewBox="0 0 100 60" width="200">
<linearGradient id="g">
<stop offset="0" stop-color="#89b4fa" />
<stop offset="1" stop-color="#cba6f7" />
</linearGradient>
<rect width="100" height="60" fill="url(#g)" />
</svg>
Entonces, ¿para qué sirve defs?
Qué aporta defs de verdad
Tres cosas, y ninguna es ocultar.
Convención. Un lector que abre el fichero sabe que todo lo que hay dentro de defs es material de referencia y que el dibujo real empieza después. En un SVG de trescientas líneas eso vale mucho.
Un sitio para lo que sí se pintaría. Aquí está el uso funcional. path, g, circle y compañía sí se renderizan. Si quieres definir una forma para instanciarla varias veces con use sin que aparezca también la original, tienes que meterla en defs, que es el único contenedor que suprime el renderizado de sus hijos.
<svg viewBox="0 0 200 60" width="400">
<defs>
<g id="pieza">
<circle cx="0" cy="0" r="12" fill="#a6e3a1" />
<circle cx="0" cy="0" r="5" fill="#11111b" />
</g>
</defs>
<use href="#pieza" x="30" y="30" />
<use href="#pieza" x="100" y="30" />
<use href="#pieza" x="170" y="30" />
</svg>
Sin el defs, el grupo original se pintaría en el origen del lienzo además de las tres instancias.
Un punto de anclaje para las herramientas. Los optimizadores tratan defs de forma especial: buscan ahí las definiciones sin usar para eliminarlas, y saben que un id dentro de defs es probablemente un objetivo de referencia y no un simple nombre.
defs no es display none
La distinción importa porque las dos cosas se parecen y se comportan distinto.
Un elemento con display: none no se renderiza y no se puede referenciar como contenido pintado. Un clipPath con display: none sigue funcionando como recorte, porque su papel no es pintarse. Pero un g con display: none referenciado por un use no aparece, porque lo que el use instancia es contenido renderizable y el display: none se hereda al clon.
Ese es un bug clásico y merece la regla explícita: el contenido que va a instanciar un use se esconde metiéndolo en defs, nunca con display: none. visibility: hidden tiene el mismo problema, y con un matiz peor: se puede revertir en un descendiente, así que produce resultados a medias.
La contrapartida es que un elemento dentro de defs sí participa en la cascada. Una regla circle { fill: red } alcanza a los círculos que hay dentro de defs, y su valor computado viaja al clon. Eso es útil (te permite tematizar el original) y sorprendente la primera vez que un selector genérico afecta a algo que creías fuera del documento.
El orden de declaración no importa
Las referencias por id en SVG se resuelven contra el documento completo, no contra lo que ya se ha parseado. Un fill="url(#g)" funciona aunque el gradiente esté declarado más abajo.
<svg viewBox="0 0 100 60" width="200">
<rect width="100" height="60" fill="url(#g)" />
<defs>
<linearGradient id="g">
<stop offset="0" stop-color="#f38ba8" />
<stop offset="1" stop-color="#fab387" />
</linearGradient>
</defs>
</svg>
Esto es distinto de HTML, donde el orden sí manda para muchas cosas, y es lo que permite que las herramientas de exportación coloquen el bloque defs al final sin romper nada. Dicho esto, la convención de ponerlo al principio existe por una razón práctica: cuando lees un fichero de arriba abajo, ver primero las definiciones te ahorra saltar.
Un caso donde el orden sí importa: si dos elementos comparten el mismo id, gana el primero en el documento. Es la puerta de entrada al problema de colisión de identificadores, que se trata a fondo en las referencias por id y el ámbito global.
Las herramientas de exportación dejan defs vacíos con frecuencia, y los optimizadores los eliminan. Hasta ahí, ruido. El problema aparece cuando el defs no está vacío pero su contenido solo se usa desde fuera del fichero.
El caso concreto: tienes un sprite donde los gradientes viven en un SVG y los símbolos que los usan en otro. Referenciar url(#g) desde un documento distinto no funciona en ningún motor actual: las referencias de pintura por fragmento tienen que resolverse dentro del mismo documento. Con use sí se puede apuntar a otro fichero, pero con fill="url()", filter="url()", clip-path="url()" y mask="url()" no.
Consecuencia práctica de dos direcciones. Primera: si divides un sprite grande en varios ficheros, todos los gradientes, filtros, máscaras y patrones que use un símbolo tienen que viajar con él en el mismo fichero, aunque eso signifique duplicarlos. Segunda, y más traicionera: un optimizador que procesa cada fichero por separado ve un gradiente que nadie referencia dentro de ese fichero y lo elimina como código muerto. El icono se queda sin relleno y el fallo aparece solo en producción, porque en desarrollo el paso de optimización no corre. Si tu pipeline optimiza SVG, esta es la primera cosa que hay que verificar en un sprite con gradientes.
Construye un SVG con un grupo definido en defs que se instancia cuatro veces con use, y añade una regla CSS que cambie el color del grupo original. Comprueba que el cambio afecta a las cuatro instancias. Después mueve el grupo fuera de defs y añádele display: none: verás desaparecer las instancias. Anota qué te dice ese experimento sobre en qué momento se resuelve el clonado.