SVG es XML en el DOM, no un formato de imagen
Situar SVG como lo que es: un lenguaje de marcado con sus reglas de XML, su espacio de nombres y sus elementos vivos en el árbol del documento.
La costumbre de guardar los SVG en la carpeta de imágenes y de pasarlos al diseñador junto a los PNG hace que casi todo el mundo empiece con el modelo mental equivocado. Un SVG no es un fichero de imagen que el navegador descodifica: es un documento con elementos, atributos, estilos, eventos y APIs, exactamente igual que el HTML que lo rodea. Todo lo que se puede hacer con SVG y todo lo que falla al usarlo se deriva de esa única frase.
- Explicar en qué se diferencia un documento SVG de un mapa de bits.
- Aplicar las reglas de XML que el HTML perdona y el SVG no.
- Manejar el espacio de nombres al crear y modificar elementos por código.
- Reconocer qué APIs del DOM cambian de comportamiento sobre elementos SVG.
No es un formato de imagen
Un PNG contiene píxeles. Un SVG contiene esto:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<title>Un circulo con borde</title>
<circle cx="50" cy="50" r="40" fill="#89b4fa" stroke="#1e66f5" stroke-width="4"/>
</svg>
Eso es un árbol de nodos. El navegador lo analiza con el mismo tipo de analizador que usa para el HTML, construye un DOM, resuelve el CSS que le corresponda, calcula geometría y pinta. El resultado es una imagen, pero el objeto que hay en memoria no es una imagen: es un documento.
Las consecuencias prácticas de esa diferencia forman la lista completa de por qué se elige SVG:
Cada forma es un elemento. Se puede seleccionar con querySelector, se le pueden poner clases, se le puede añadir un escuchador de eventos y aparece en el inspector con su caja y sus estilos.
El CSS se aplica de verdad. No un CSS especial: el mismo, con la misma cascada, la misma herencia y las mismas variables. Un :hover sobre un path funciona porque el path es un elemento.
El texto sigue siendo texto. Se busca con la búsqueda del navegador, se selecciona, se copia y lo lee un lector de pantalla.
El fichero es texto plano. Se compara en un control de versiones, se genera desde una plantilla del servidor, se comprime muy bien con gzip y se puede editar con un editor de texto.
Frente al canvas, que es el modelo inmediato del que sale la amnesia total descrita en el modo inmediato, SVG es el modelo retenido: el navegador guarda el árbol y lo vuelve a pintar cuando algo cambia, sin que tú tengas que redibujar nada. Cambias un atributo y la pantalla se actualiza.
Las reglas de XML que te van a morder
El HTML es tolerante por diseño. El SVG viene del XML, que no lo es en absoluto, y aunque el analizador de HTML disimula parte de la diferencia, hay cuatro reglas que acaban apareciendo.
Todo elemento se cierra. No hay etiquetas opcionales ni implícitas. <circle/> o <circle></circle>, pero nunca un <circle> suelto.
Los nombres distinguen mayúsculas de minúsculas. Y varios atributos importantes son camelCase: viewBox, preserveAspectRatio, patternUnits, gradientUnits, clipPathUnits, markerWidth, textLength, lengthAdjust. Escribir viewbox en minúsculas es un atributo distinto que no existe y que el navegador ignora en silencio.
Aquí hay un matiz que confunde a mucha gente: dentro de un documento HTML, el analizador corrige la capitalización de una lista conocida de atributos de SVG, así que <svg viewbox="0 0 24 24"> escrito a mano en el HTML funciona. Esa amabilidad desaparece en cuanto el mismo marcado vive en un fichero .svg, o en cuanto lo asignas desde JavaScript con setAttribute, donde la cadena se usa literalmente. Escribir siempre la forma correcta es la única política que no falla.
Los caracteres reservados van escapados. Un & suelto en un texto rompe el documento entero; en un fichero SVG independiente, un error de sintaxis produce una pantalla de error del analizador de XML en lugar de un dibujo.
El orden es el orden de pintado. No hay z-index en SVG: lo que va después se pinta encima. Reordenar es mover nodos en el árbol.
El espacio de nombres
XML permite mezclar vocabularios en un mismo documento, y para distinguirlos usa espacios de nombres. El de SVG es http://www.w3.org/2000/svg, y de ahí salen dos obligaciones muy concretas.
En un fichero .svg independiente, el atributo xmlns es obligatorio. Sin él, el documento no es SVG para el navegador y no se dibuja nada. Dentro de un documento HTML no hace falta, porque el analizador de HTML asigna el espacio de nombres automáticamente al ver la etiqueta de apertura.
Al crear elementos por código hay que usar la función con espacio de nombres.
// Correcto
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const circulo = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circulo.setAttribute('cx', '50');
circulo.setAttribute('cy', '50');
circulo.setAttribute('r', '40');
svg.appendChild(circulo);
document.body.appendChild(svg);
Existe también setAttributeNS, que solo hace falta para los pocos atributos que pertenecen a otro espacio de nombres. El caso que queda es el heredado xlink:href, sustituido en SVG 2 por un href normal que ya funciona en todos los navegadores actuales; si mantienes código antiguo, elemento.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', valor) es la forma correcta de escribirlo.
Este es el rito de paso de SVG y merece entenderlo entero porque el fallo es silencioso y el marcado resultante parece perfecto en el inspector. Si escribes document.createElement('circle') en lugar de createElementNS, obtienes un nodo con el nombre correcto, con los atributos correctos, colocado en el sitio correcto del árbol, y que no dibuja absolutamente nada. El motivo es que ese nodo pertenece al espacio de nombres del HTML, no al de SVG: para el navegador es un elemento HTML desconocido llamado “circle”, algo tan sin sentido como un <blorp>, y los elementos desconocidos no tienen ninguna representación gráfica. En el inspector se ve igual que sus hermanos correctos, y por eso la depuración se atasca; la forma de confirmarlo en un segundo es leer elemento.namespaceURI, que devolverá la dirección del HTML en vez de la del SVG. Hay dos variantes de la misma familia y las tres juntas explican casi todos los “mi SVG generado no aparece”. La primera: setAttribute con el nombre mal capitalizado. svg.setAttribute('viewbox', '0 0 24 24') no lanza ningún error y no hace nada, porque desde JavaScript no hay corrección de capitalización: creas un atributo llamado viewbox que nadie mira. El síntoma es un SVG que se dibuja pero no escala. La segunda: usar innerHTML sobre un elemento SVG. Funciona en los navegadores actuales sobre documentos HTML, y por eso se usa mucho, pero es la vía por la que se cuelan cadenas de marcado sin sanear y depende del analizador de HTML, con sus reglas de corrección. Si necesitas insertar un fragmento de SVG desde una cadena, la opción robusta es analizarlo explícitamente como XML con new DOMParser().parseFromString(cadena, 'image/svg+xml') e importar los nodos con document.importNode, que respeta el espacio de nombres y falla ruidosamente si el XML está mal en lugar de dibujar a medias. Y una tercera consecuencia que no es de creación sino de lectura, y que rompe código que parecía correcto: elemento.className en un elemento SVG no es una cadena, es un objeto animado con una propiedad baseVal, así que cualquier if (el.className.includes(...)) falla en silencio. La solución es la de siempre y funciona en ambos mundos: classList, que sí está implementado en los elementos SVG.
Elementos SVG en el árbol
Un elemento SVG es un SVGElement, no un HTMLElement, y esa diferencia de interfaz explica un puñado de comportamientos que sorprenden.
| Lo que quieres | En HTML | En SVG |
|---|---|---|
| Leer o cambiar clases | className o classList |
classList (nunca className) |
| Estilos en línea | elemento.style |
elemento.style, igual |
| Datos propios | dataset |
dataset, igual |
| Medir la caja | getBoundingClientRect |
getBoundingClientRect en píxeles de pantalla, o getBBox en unidades de usuario |
| Eventos | addEventListener |
addEventListener, igual |
| Foco | tabindex |
tabindex, funciona en SVG 2 |
La fila de la medición es la que más se usa. getBBox devuelve la caja del contenido en unidades del sistema de coordenadas del elemento, sin contar trazo ni filtros, y es la herramienta para ajustar un viewBox al contenido real o para colocar una etiqueta. getBoundingClientRect devuelve píxeles de pantalla, ya con todas las transformaciones aplicadas, y sirve para colocar un elemento HTML encima.
const p = document.querySelector('#trazado');
const caja = p.getBBox(); // unidades de usuario, sin trazo
const enPantalla = p.getBoundingClientRect(); // pixeles CSS, con transformaciones
Y la consecuencia de todo el capítulo, que conviene fijar antes de seguir: un SVG que forma parte del documento tiene todas las capacidades que acabamos de enumerar; un SVG cargado como imagen no tiene ninguna. Esa bifurcación es el asunto de la última lección del nivel, y es la fuente número uno de confusión con SVG.