symbol y su propio viewBox
Por qué symbol es el contenedor correcto de un sprite: viewBox individual por icono, preserveAspectRatio por instancia, y tamaño controlado desde el use.
Un sprite hecho con g y otro hecho con symbol se parecen hasta que intentas mezclar iconos de rejillas distintas o redimensionar una instancia. symbol es el único contenedor que trae su propio viewBox, y eso convierte cada icono en un mini documento con su sistema de coordenadas independiente. Sin esa propiedad, un sprite con iconos de 16, 24 y 32 unidades es un ejercicio de aritmética manual; con ella, no hay nada que calcular.
- Explicar qué añade
symbolsobre ungdentro dedefs. - Mezclar iconos de rejillas distintas en un mismo sprite sin transformaciones manuales.
- Controlar el ajuste de cada instancia con
preserveAspectRatioen eluse. - Decidir el tamaño de un icono desde CSS sin tocar el marcado.
Un mini documento por icono
symbol no se renderiza nunca por sí solo, igual que defs. Su particularidad es que acepta viewBox y preserveAspectRatio, y que cuando se instancia con use se comporta como si fuera un elemento svg anidado: establece un viewport nuevo, con las dimensiones que le dé el use, y ajusta su viewBox dentro.
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="i-24" viewBox="0 0 24 24">
<path d="M12 2 L22 22 L2 22 Z" fill="currentColor" />
</symbol>
<symbol id="i-16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" fill="currentColor" />
</symbol>
</svg>
<svg width="32" height="32" style="color:#89b4fa"><use href="#i-24" /></svg>
<svg width="32" height="32" style="color:#a6e3a1"><use href="#i-16" /></svg>
Los dos iconos salen del mismo tamaño en pantalla aunque uno esté dibujado en una rejilla de 24 y el otro en una de 16. Cada symbol escala su contenido a la caja que le da el use. Con un g en lugar de un symbol, el segundo icono saldría a dos tercios del tamaño del primero y habría que corregirlo con un transform="scale(1.5)" escrito a mano.
Esa es la razón concreta por la que los sprites se hacen con symbol. No es convención: es la única forma de que iconos de procedencias distintas convivan sin aritmética.
De dónde sale el tamaño de la instancia
Cuando se instancia un symbol, el tamaño del viewport que se crea se decide con esta prioridad:
- Los atributos
widthyheightdeluse, si los tiene. - Los atributos
widthyheightdelsymbol, si los tiene. - El cien por cien en ambas dimensiones, que se resuelve contra el viewport que contiene al
use.
En la práctica el caso tres es el que se usa: el symbol no declara tamaño, el use tampoco, y el icono llena el svg que lo envuelve. El tamaño se controla entonces desde CSS sobre ese svg exterior:
.ico { width: 1.25rem; height: 1.25rem; }
.ico-grande { width: 2rem; height: 2rem; }
Es el patrón correcto porque deja el tamaño en la capa donde vive el resto de la maquetación, y porque em y rem funcionan: un icono dimensionado en em escala con el texto que lo acompaña, que es exactamente lo que quieres en un botón.
/* Icono que crece con el texto de su boton */
.boton .ico { width: 1em; height: 1em; vertical-align: -0.125em; }
Ese vertical-align negativo compensa la diferencia entre la caja del icono y la línea base del texto. Sin él, el icono se alinea por su borde inferior con la línea base y queda demasiado alto. El valor exacto depende de la fuente; entre -0.1em y -0.15em funciona con casi todas.
preserveAspectRatio por instancia
symbol acepta preserveAspectRatio, y use puede sobrescribirlo. Eso permite que la misma forma se ajuste de maneras distintas según dónde se use.
El valor por omisión es xMidYMid meet: mantiene la proporción y encaja el contenido entero dentro de la caja, centrado. Es lo que quieres para un icono.
slice hace lo contrario: mantiene la proporción y llena la caja, recortando lo que sobra. Es lo que quieres para un patrón decorativo o una ilustración de fondo.
none deforma para llenar exactamente. Casi nunca es lo que quieres, con una excepción: un separador ondulado que debe estirarse a lo ancho del contenedor sin cambiar de altura.
<!-- El mismo simbolo, tres ajustes -->
<svg width="80" height="40"><use href="#i-24" /></svg>
<svg width="80" height="40"><use href="#i-24" preserveAspectRatio="xMidYMid slice" /></svg>
<svg width="80" height="40"><use href="#i-24" preserveAspectRatio="none" /></svg>
En una caja no cuadrada, el primero deja bandas vacías a los lados, el segundo recorta arriba y abajo, y el tercero achata la forma.
Un symbol instanciado establece un viewport, y un viewport recorta su contenido por omisión, porque el valor inicial de overflow para un elemento que establece viewport es hidden.
Eso significa que cualquier cosa que sobresalga del viewBox del símbolo desaparece. Y lo que sobresale con más frecuencia no es la geometría, es el trazo: un icono dibujado exactamente en la rejilla de 24 con stroke-width="2" pierde una unidad de trazo en cada borde que toque el límite. El icono se ve con los lados aplanados y nadie entiende por qué, porque en el fichero original, abierto suelto, se veía bien: fuera del symbol no había viewport que recortara.
Hay tres arreglos y conviene elegir el correcto. El primero, y el bueno: diseñar los iconos con margen, dejando una o dos unidades de aire en la rejilla. Es lo que hacen todas las bibliotecas serias y por eso sus iconos de 24 dibujan dentro de una caja útil de 20. El segundo: ampliar el viewBox del símbolo para que incluya el desbordamiento, lo que cambia la escala aparente del icono respecto a sus vecinos. El tercero: overflow: visible sobre el símbolo, que funciona pero deja que el icono pinte fuera de su caja y se solape con lo que tenga al lado.
Y una precisión sobre el mismo fenómeno un nivel más arriba: el svg exterior también recorta. Un filtro con desbordamiento o una sombra aplicada al use se corta contra la caja del svg, no contra la del símbolo. Ese caso se arregla con overflow: visible en el svg exterior, y es la causa habitual de las sombras cortadas en iconos.
symbol frente a svg anidado
Hay una alternativa que hace casi lo mismo: un elemento svg dentro de defs, referenciado por use. También establece viewport, también acepta viewBox. Las diferencias:
symbol |
svg anidado |
|
|---|---|---|
Se renderiza sin use |
No | Sí |
Acepta viewBox |
Sí | Sí |
Necesita defs alrededor |
No | Sí, para no pintarse |
| Semántica para herramientas | Es un símbolo de sprite | Es un documento anidado |
symbol es más limpio porque expresa la intención y no necesita el defs. La única razón para usar un svg anidado es si el contenido tiene que renderizarse también en su sitio original, y en un sprite eso no pasa nunca.
Un apunte sobre accesibilidad que se cierra en el nivel 24: un title dentro de un symbol se clona en cada árbol de sombra y se convierte en el nombre accesible de cada instancia. Eso suena bien y produce un problema, porque el mismo icono puede necesitar nombres distintos según dónde se use, y el que viene del símbolo no se puede sobrescribir. La práctica correcta es no poner title en el símbolo y poner el nombre en el svg que envuelve al use.
Construye un sprite con tres iconos dibujados en rejillas de 16, 24 y 32 unidades, y comprueba que los tres salen del mismo tamaño con el mismo svg contenedor. Después dale a uno de ellos un stroke-width que lo haga desbordar su viewBox y observa el recorte. Corrígelo de las tres maneras del callout y decide cuál preferirías en un sistema de diseño con cincuenta iconos.