wandres.dev
SVG: EL DOCUMENTO · Un lenguaje, no un formato

El viewBox y qué hace exactamente

Entender el viewBox como la transformación implícita que relaciona el lienzo interno con el hueco en la página, y usarla para escalar, encuadrar y hacer zoom.

⏱ 18 min

El viewBox es el atributo que convierte un SVG en algo que se adapta, y también el que más se copia sin entender. No recorta, no define un tamaño y no es una ventana: declara qué rectángulo del sistema de coordenadas interno debe hacerse corresponder con el hueco que el elemento ocupa en la página. De esa correspondencia sale una transformación que el navegador aplica a todo el contenido, y casi todos los comportamientos raros de SVG se explican mirando esa transformación.

🎯 Al terminar esta lección sabrás
  • Leer los cuatro números de un viewBox y saber qué hace cada uno.
  • Calcular la transformación implícita que produce, incluida la escala.
  • Usar el viewBox para encuadrar contenido y para hacer zoom y desplazamiento.
  • Reconocer los cuatro errores clásicos, empezando por el grosor del trazo.

Los cuatro números

<svg viewBox="0 0 24 24" width="48" height="48"> ... </svg>

Los cuatro valores son, en orden, min-x, min-y, ancho y alto, y describen un rectángulo en unidades de usuario, el sistema de coordenadas interno del dibujo.

Frente a ese rectángulo hay otro: el viewport, que es el hueco que el elemento ocupa en la página. Ese hueco sale de los atributos width y height, o del CSS que le apliques, o del contenedor cuando el SVG está en línea y no tiene tamaño propio.

El viewBox declara que ese rectángulo interno debe verse ocupando ese hueco. Todo lo demás es consecuencia.

En el ejemplo de arriba, un cuadrado de 24 por 24 unidades se muestra en 48 por 48 píxeles: el factor de escala es dos. Un circle con r="12" se verá con radio de 24 píxeles, sin que su atributo cambie. Las unidades del dibujo dejan de tener relación con los píxeles, y eso es exactamente lo que se quiere: el dibujo se describe una vez, en las unidades que le vengan bien, y se muestra al tamaño que haga falta.

La transformación implícita

Con preserveAspectRatio en su valor por defecto —que se estudia en la lección siguiente— la transformación se calcula así:

// viewport: el hueco en pixeles. vb: los cuatro numeros del viewBox
function transformacionDeViewBox(viewport, vb) {
  const sx = viewport.ancho / vb.ancho;
  const sy = viewport.alto / vb.alto;
  const escala = Math.min(sx, sy);                 // uniforme: el menor de los dos
  const tx = (viewport.ancho - vb.ancho * escala) / 2 - vb.x * escala;
  const ty = (viewport.alto - vb.alto * escala) / 2 - vb.y * escala;
  return { escala, tx, ty };                        // equivale a translate(tx,ty) scale(escala)
}

Tres cosas se leen directamente de esa función.

La escala es uniforme y se queda con la menor de las dos proporciones. Por eso el contenido nunca se deforma por defecto, y por eso si las proporciones del viewBox y del hueco no coinciden sobra espacio en un eje.

El sobrante se reparte por igual a los dos lados, que es lo que centra el dibujo.

min-x y min-y desplazan el origen. Un viewBox="-50 -50 100 100" pone el origen de coordenadas en el centro del dibujo, lo cual es comodísimo para cualquier cosa radial: un reloj, un gráfico de tarta, una rosa de los vientos. Las coordenadas negativas dejan de ser un problema y pasan a ser la mitad del lienzo.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 100 100" width="200" height="200">
  <circle r="48" fill="none" stroke="#89b4fa" stroke-width="2"/>
  <line x1="0" y1="0" x2="0" y2="-38" stroke="#cdd6f4" stroke-width="3"/>
  <line x1="0" y1="0" x2="26" y2="0" stroke="#cdd6f4" stroke-width="2"/>
  <circle r="3" fill="#f38ba8"/>
</svg>

Sin el viewBox centrado, ese mismo reloj exigiría sumar 50 a cada coordenada de cada línea.

Encuadrar, escalar y hacer zoom

De la transformación salen tres usos que son la razón de existir del atributo.

Adaptarse al contenedor. Un SVG con viewBox y sin width ni height tiene proporción intrínseca pero no tamaño, así que ocupa el ancho disponible y calcula el alto. Es el comportamiento que se quiere el noventa por ciento de las veces:

<div style="max-width:640px">
  <svg viewBox="0 0 16 9" style="width:100%;height:auto;display:block">
    <rect width="16" height="9" fill="#313244"/>
    <circle cx="8" cy="4.5" r="3" fill="#a6e3a1"/>
  </svg>
</div>

El display: block no es decorativo: sin él, el SVG es un elemento en línea y arrastra el espacio bajo la línea base, que aparece como unos píxeles de hueco misterioso bajo la imagen.

Ajustar el encuadre al contenido real. Si el dibujo no llena su viewBox, aparece con márgenes que nadie pidió. La medida exacta se obtiene del propio DOM:

const svg = document.querySelector('svg');
const b = svg.getBBox();                 // caja del contenido en unidades de usuario
const m = 4;                             // margen en las mismas unidades
svg.setAttribute('viewBox', `${b.x - m} ${b.y - m} ${b.width + m * 2} ${b.height + m * 2}`);

getBBox ignora el grosor del trazo, así que el margen tiene que cubrir al menos la mitad del trazo más grueso o las líneas de los bordes saldrán cortadas.

Zoom y desplazamiento. Cambiar el viewBox es la forma más barata de hacer zoom en SVG: no hay que transformar nada, solo escribir cuatro números.

const svg = document.querySelector('#mapa');
let vista = { x: 0, y: 0, ancho: 800, alto: 600 };

function aplicar() {
  svg.setAttribute('viewBox', `${vista.x} ${vista.y} ${vista.ancho} ${vista.alto}`);
}

svg.addEventListener('wheel', e => {
  e.preventDefault();
  const caja = svg.getBoundingClientRect();
  // Punto bajo el cursor en unidades de usuario
  const ux = vista.x + (e.clientX - caja.left) / caja.width * vista.ancho;
  const uy = vista.y + (e.clientY - caja.top) / caja.height * vista.alto;
  const factor = Math.exp(e.deltaY * 0.0015);
  vista.ancho *= factor;
  vista.alto *= factor;
  // Mantener bajo el cursor el mismo punto del dibujo
  vista.x = ux - (e.clientX - caja.left) / caja.width * vista.ancho;
  vista.y = uy - (e.clientY - caja.top) / caja.height * vista.alto;
  aplicar();
}, { passive: false });

Ese cálculo —convertir el punto del cursor a unidades de usuario, escalar, y reposicionar para que el punto siga bajo el cursor— es el zoom centrado en el puntero, y es el mismo razonamiento que en canvas resuelve la matriz inversa según invertir la transformación. Aquí sale más barato porque el navegador rehace el dibujo por ti.

El viewBox escala absolutamente todo, incluido el grosor del trazo, y ahí se rompen los iconos

La escala del viewBox no se aplica solo a las posiciones: se aplica al espacio entero. Un stroke-width="1" en un SVG de viewBox="0 0 24 24" mostrado a 24 píxeles se ve como una línea de un píxel; el mismo SVG mostrado a 480 píxeles se ve como una línea de veinte. Eso es lo correcto para una ilustración —todo crece junto, que es la promesa del vector— y es exactamente lo que no se quiere en tres situaciones muy habituales. La primera: los iconos de una biblioteca a distintos tamaños. Un juego de iconos diseñado en un lienzo de 24 con trazo de 1,5 se ve perfecto a 24 y demasiado fino a 64, porque el trazo crece igual que el dibujo pero la percepción del peso visual no es lineal. Los sistemas de iconos serios resuelven esto con rejillas ópticas separadas: dibujos distintos para los tamaños grandes y pequeños, no el mismo escalado. Si solo tienes un dibujo, el apaño es exponer el grosor como una variable CSS y ajustarlo por tamaño. La segunda: las líneas de un mapa con zoom. Al ampliar diez veces, las carreteras se convierten en franjas enormes y el mapa se vuelve ilegible. La solución existe y es una propiedad poco conocida: vector-effect: non-scaling-stroke, que hace que el grosor del trazo se calcule en el espacio de la pantalla y no en el del usuario, con lo que la línea conserva su grosor visual a cualquier zoom. Está disponible en todos los navegadores actuales y es la respuesta correcta para bordes de selección, guías, retículas y cualquier trazo que sea información y no dibujo. La tercera, y es la que produce fallos más desconcertantes: los filtros y las sombras. La región de un filtro y el radio de un desenfoque también están en unidades de usuario, con lo que un desenfoque calibrado en el lienzo de diseño puede quedar imperceptible o desmesurado al escalar. Y hay un detalle de rendimiento asociado que sorprende: el coste de rasterizar un filtro depende del tamaño en píxeles reales, no en unidades de usuario, así que el mismo SVG con un desenfoque suave puede ser gratis como icono de 24 píxeles y costar decenas de milisegundos como fondo de pantalla completa. Si un SVG concreto hunde el rendimiento solo cuando se muestra grande, mira los filtros antes que ninguna otra cosa.

Los errores clásicos

Creer que el viewBox recorta. No recorta él: recorta el elemento. La hoja de estilos del navegador aplica overflow: hidden al svg raíz, y por eso lo que queda fuera del rectángulo no se ve. Se puede cambiar con overflow: visible, y entonces el contenido se sale del hueco, lo que es útil para efectos que sobresalen y muy confuso si ocurre sin querer.

Poner width y height en el marcado y también en el CSS. Gana el CSS, y el atributo se queda como tamaño por defecto para cuando el CSS no diga nada. No es un error en sí, pero explica por qué a veces cambiar el atributo no hace nada.

Quitar el viewBox al optimizar. Algunas herramientas de optimización eliminan el viewBox cuando hay width y height, y con eso el SVG deja de escalar: se queda clavado a su tamaño y cualquier CSS que intente cambiarlo produce un dibujo recortado en vez de escalado. Es una de las razones por las que hay que revisar la configuración de esas herramientas.

Confundir las unidades del viewBox con píxeles. No lo son. Si tu viewBox es de 1000 unidades de ancho y el hueco mide 200 píxeles, un objeto de 10 unidades mide 2 píxeles. Todo el dibujo interno vive en su propia escala, y esa independencia es justamente lo que hace que el mismo fichero sirva para un icono y para un cartel.