img, object o en línea: qué permite cada forma de incluir un SVG
La decisión que determina qué CSS se aplica, qué scripts corren y qué recursos se cargan, y por qué tu hoja de estilos no afecta al SVG que acabas de insertar.
La pregunta más repetida sobre SVG es por qué el CSS no le afecta, y la respuesta nunca está en el CSS: está en cómo se incluyó el fichero. Las tres formas de meter un SVG en una página producen tres situaciones radicalmente distintas en cuanto a estilos, scripts, recursos externos y accesibilidad, y elegir mal condena todo el trabajo posterior. La diferencia no es un capricho de implementación, es una frontera de seguridad, y entenderla como tal hace que todas las consecuencias se vuelvan predecibles.
- Enumerar qué permite y qué prohíbe cada una de las tres formas.
- Explicar por qué el CSS de la página no alcanza a un SVG cargado como imagen.
- Elegir la forma correcta según lo que el SVG tenga que hacer.
- Reconocer los dos problemas propios del SVG en línea y sus soluciones.
Las tres formas
Como imagen. El SVG es el origen de una etiqueta de imagen, o el valor de una propiedad de CSS como background-image o mask-image.
<img src="/iconos/mapa.svg" alt="Mapa de la zona" width="48" height="48">
El navegador lo trata como una imagen de tamaño arbitrario: lo descarga, lo dibuja en su caja y ahí acaba la relación. Se cachea como cualquier imagen, admite loading="lazy", y su contenido está completamente aislado de la página.
Como documento embebido. Con object, iframe o el heredado embed.
<object data="/graficos/ventas.svg" type="image/svg+xml"
width="600" height="400" aria-label="Grafico de ventas trimestrales">
<img src="/graficos/ventas.png" alt="Grafico de ventas trimestrales">
</object>
Aquí sí hay un documento de verdad, con su propio árbol, sus propios estilos y sus propios scripts, dentro de la página pero separado de ella. Lo que va entre las etiquetas de object es contenido alternativo que se muestra si el recurso no carga.
En línea. El marcado del SVG vive dentro del HTML.
<svg viewBox="0 0 24 24" width="24" height="24" fill="none"
stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M3 12h18M12 3v18"/>
</svg>
No hay fichero, no hay petición y no hay frontera: esos elementos son nodos del mismo documento que todo lo demás, con las capacidades completas descritas en SVG es XML en el DOM.
Por qué el CSS de la página no llega
Un SVG cargado como imagen se dibuja en un contexto aparte al que la cascada de tu página no entra. No es que el selector no lo alcance: es que hay dos documentos, y las hojas de estilo de uno no se aplican al otro. Por eso este código, que todo el mundo escribe alguna vez, no hace absolutamente nada:
/* No funciona: el path esta en otro documento */
img[src$=".svg"] path { fill: red; }
La misma frontera explica las otras tres sorpresas.
currentColor no funciona. Dentro de la imagen no hay ningún color heredado de tu página, así que currentColor se resuelve contra el valor por defecto del propio documento SVG, que es negro salvo que el fichero diga otra cosa.
Las fuentes externas no se cargan. Un SVG en modo imagen no puede pedir recursos externos, así que la fuente declarada con @font-face no llega y el texto se dibuja con la de reserva, normalmente con otra anchura y descolocando el diseño. Es la causa clásica de un SVG exportado que se ve perfecto al abrirlo directamente y roto dentro de la página. Las salidas son dos: incrustar la fuente en el propio fichero como URI de datos, o convertir el texto a trazados al exportar, con lo que deja de ser texto.
Los scripts no se ejecutan. Nada de JavaScript dentro de un SVG cargado como imagen. Y esto no es una limitación molesta sino la característica que lo hace utilizable: es lo que permite mostrar un SVG subido por un usuario sin abrir un agujero de seguridad.
Conviene entender por qué existe esa frontera, porque de ahí se deduce todo lo demás sin memorizar nada. Un SVG es un documento con capacidad de ejecutar scripts, cargar recursos y definir estilos. Si el CSS y el JavaScript de tu página pudieran entrar en un SVG remoto, o si el SVG pudiera salir hacia tu página, cualquier imagen sería un vector de ejecución de código en el origen del sitio. La plataforma lo resuelve igual que con las imágenes normales: cuando un SVG entra como imagen, se le retiran las capacidades de documento. De ahí, en orden, el aislamiento del CSS, el bloqueo de recursos externos y la desactivación de los scripts. La consecuencia de seguridad que hay que tener grabada es la inversa: si insertas en línea un SVG que no controlas —subido por un usuario, traído de una API, pegado desde un editor— estás metiendo marcado ejecutable en tu documento. Un SVG puede contener un elemento script, manejadores de eventos en atributos y referencias externas, y al ponerlo en línea todo eso corre con los permisos de tu origen: es exactamente una inyección de código. La forma segura de mostrar un SVG ajeno es como imagen, que lo neutraliza por construcción; y si de verdad necesitas insertarlo en línea, hay que sanearlo con una biblioteca que entienda SVG, nunca con un reemplazo de cadenas. Ahora los dos regalos, que compensan parte de lo que se pierde y son poco conocidos. El primero: dentro de un SVG cargado como imagen, las animaciones declarativas sí funcionan. Las animaciones de CSS escritas dentro del fichero se ejecutan con normalidad, porque no requieren scripts. Un indicador de carga, un logotipo que se dibuja solo o un icono que late pueden vivir enteros en un fichero cacheado y funcionar dentro de una etiqueta de imagen sin una línea de JavaScript. El segundo resuelve un problema real: las consultas de medios dentro del fichero también funcionan y responden a las preferencias del usuario. Un bloque @media (prefers-color-scheme: dark) dentro del SVG cambia sus colores cuando el sistema está en modo oscuro, aunque el fichero esté cargado como imagen y no tengas ninguna forma de estilarlo desde fuera. Es la solución estándar para un logotipo o un favicon que deben adaptarse al tema, y evita servir dos ficheros y elegir con picture. Y las consultas de dimensiones dentro del fichero se evalúan contra el viewport del propio SVG, no contra el de la página, lo que permite escribir un gráfico que muestra más o menos detalle según el tamaño al que se le pinte, sin saber nada del sitio que lo usa.
La tabla de decisión
| Capacidad | Como imagen | Con object |
En línea |
|---|---|---|---|
| El CSS de la página lo estila | No | No | Sí |
currentColor sigue al texto |
No | No | Sí |
| Las variables CSS de la página llegan | No | No | Sí |
| El CSS dentro del fichero funciona | Sí | Sí | Sí, y se mezcla con el de la página |
| Animaciones declarativas del fichero | Sí | Sí | Sí |
| Scripts dentro del fichero | No | Sí | Sí, con los permisos de la página |
| Recursos externos: fuentes, imágenes | No | Sí | Sí |
| Accesible desde el JavaScript de la página | No | Sí, con contentDocument |
Sí, directamente |
| Se cachea por separado | Sí | Sí | No: viaja en el HTML |
| Petición adicional | Sí | Sí | No |
| Seguro para contenido ajeno | Sí | Con aislamiento | No |
| Texto alternativo | alt |
Contenido de reserva y aria-label |
title y desc internos |
Traducido a recomendaciones, sin ambigüedad:
Iconos que deben tomar el color del texto: en línea. Es el único caso en el que currentColor funciona, y es la razón principal por la que los sistemas de iconos modernos son en línea.
Ilustraciones, logotipos y fondos decorativos: como imagen. Se cachean, no engordan el HTML, se pueden cargar de forma diferida y no necesitan estilado.
Gráficos con interacción propia y autocontenida, sobre todo si vienen de un tercero o de otro equipo: con object. Corren sus scripts, no contaminan tu página y no la contaminan con sus identificadores.
Gráficos que la aplicación tiene que manipular: en línea, generados o modificados desde tu código.
SVG subido por usuarios: como imagen, siempre.
Un apunte sobre object que se pasa por alto: desde la página sí se puede llegar a su interior, si el fichero es del mismo origen, y eso permite estilarlo o manipularlo después de cargado.
const obj = document.querySelector('object');
obj.addEventListener('load', () => {
const doc = obj.contentDocument; // tambien obj.getSVGDocument()
if (!doc) return; // otro origen: no hay acceso
const estilo = doc.createElementNS('http://www.w3.org/2000/svg', 'style');
estilo.textContent = '.barra { fill: #a6e3a1 }';
doc.documentElement.appendChild(estilo);
doc.querySelectorAll('.barra').forEach(b => {
b.addEventListener('click', () => console.log(b.dataset.valor));
});
});
Es la vía intermedia entre el aislamiento total de la imagen y la ausencia de aislamiento del SVG en línea, y está infrautilizada.
Los dos problemas del SVG en línea
El SVG en línea lo permite todo, y a cambio tiene dos inconvenientes propios que hay que gestionar.
No se cachea y se repite. El marcado viaja dentro del HTML, así que se descarga con cada página y ocupa sitio en el documento. Con un icono de diez líneas da igual; con un mapa de cuatrocientos kilobytes repetido en cada página es un problema de peso y de tiempo de análisis. Y si el mismo icono aparece treinta veces en una lista, el marcado se duplica treinta veces, con su coste en nodos del DOM. La solución dentro del propio documento es definir el dibujo una vez y referenciarlo desde cada aparición, que es todo un tema en sí mismo.
Los identificadores son globales. Esto sorprende y rompe cosas de verdad. Los id de un SVG en línea comparten espacio con los de todo el documento HTML, y las referencias internas —un gradiente, una máscara, un recorte, un marcador— se resuelven por id. Si insertas dos veces el mismo SVG con un gradiente llamado degradado, el segundo tiene un identificador duplicado y ambos elementos usarán el primero, porque la búsqueda devuelve el primero que encuentra. El resultado es una figura que aparece con el color de otra, y es de esos fallos que uno tarda en creerse.
<!-- Los dos usaran el gradiente del PRIMERO -->
<svg viewBox="0 0 100 100"><defs><linearGradient id="g">…</linearGradient></defs>
<rect width="100" height="100" fill="url(#g)"/></svg>
<svg viewBox="0 0 100 100"><defs><linearGradient id="g">…</linearGradient></defs>
<rect width="100" height="100" fill="url(#g)"/></svg>
La solución es prefijar los identificadores con algo único por instancia en el momento de insertar:
let contador = 0;
function insertarSvg(contenedor, marcado) {
const sufijo = '-i' + (++contador);
const doc = new DOMParser().parseFromString(marcado, 'image/svg+xml');
const raiz = doc.documentElement;
// Renombrar los id y las referencias url(#...) y href="#..."
raiz.querySelectorAll('[id]').forEach(el => {
const viejo = el.id;
const nuevo = viejo + sufijo;
el.id = nuevo;
raiz.querySelectorAll('*').forEach(otro => {
for (const atributo of otro.attributes) {
if (atributo.value.includes('#' + viejo)) {
atributo.value = atributo.value.replaceAll('#' + viejo, '#' + nuevo);
}
}
});
});
contenedor.appendChild(document.importNode(raiz, true));
}
Ese recorrido es cuadrático y solo vale para insertar unos pocos SVG; para un sistema de iconos completo, lo que se hace es generar los identificadores únicos en el momento de construir la página, no en el navegador. Y la observación general que conviene llevarse: cualquier cosa que dentro de un SVG se refiera a otra por id deja de ser autocontenida en cuanto el SVG se pone en línea, y eso incluye gradientes, patrones, máscaras, recortes, filtros y marcadores.