text y su posicionamiento
x e y marcan la línea base, no la esquina. text-anchor, dominant-baseline y sus diferencias entre motores, las listas de coordenadas por carácter, y el centrado que siempre funciona.
En HTML, colocar texto es colocar una caja. En SVG es colocar un punto de anclaje sobre una línea base, y esa diferencia explica por qué el primer text que escribe cualquiera aparece medio fuera del lienzo. Con dos propiedades se controla el anclaje en los dos ejes, pero una de las dos tiene un historial de implementaciones divergentes que obliga a conocer la alternativa manual.
- Situar un
textsabiendo quexeymarcan el origen de la línea base. - Usar
text-anchorpara alinear en horizontal. - Elegir entre
dominant-baseliney el desplazamiento manual enempara centrar en vertical. - Aplicar listas de coordenadas para posicionar carácter a carácter.
x, y y la línea base
<text x="10" y="20">Hola</text> coloca el origen de la línea base en el punto (10, 20). La línea base es la línea imaginaria sobre la que se apoyan las letras: la parte inferior de una o la toca, y la cola de una p la atraviesa.
De ahí sale el bug inaugural: y="0" pone la línea base en el borde superior del lienzo, así que solo se ven los descendentes. Todo lo demás queda fuera.
<svg viewBox="0 0 200 60" width="400">
<line x1="0" y1="40" x2="200" y2="40" stroke="#45475a" />
<text x="10" y="40" font-size="28" fill="#cdd6f4"
font-family="system-ui, sans-serif">Apoyo</text>
</svg>
La línea marca exactamente dónde está la base. La p la cruza; la A se apoya en ella.
Para saber cuánto sube el texto por encima de la línea base hace falta la métrica de la fuente: el ascendente. Como regla práctica, en la mayoría de fuentes sin gracia el ascendente ronda el 0,8 del tamaño de fuente. Un texto de 28 unidades ocupa unas 22 por encima de la base, así que para que quede pegado al borde superior con un margen de 4, la y debe ser 26.
Ese cálculo aproximado se puede sustituir por uno exacto pidiendo la caja al DOM:
const t = document.querySelector('text');
const caja = t.getBBox(); // x, y, width, height de la geometria pintada
getBBox() sobre un text devuelve la caja del texto rasterizado, con lo que la diferencia entre caja.y y el atributo y es exactamente el ascendente usado.
text-anchor
text-anchor decide qué punto del texto se alinea con la x declarada. Tres valores:
start(por omisión): laxes el borde inicial del texto. En un idioma de izquierda a derecha, el borde izquierdo.middle: laxes el centro.end: laxes el borde final.
<svg viewBox="0 0 200 100" width="400">
<line x1="100" y1="0" x2="100" y2="100" stroke="#45475a" stroke-dasharray="2 3" />
<g font-size="16" fill="#cdd6f4" font-family="system-ui, sans-serif">
<text x="100" y="30" text-anchor="start">start</text>
<text x="100" y="55" text-anchor="middle">middle</text>
<text x="100" y="80" text-anchor="end">end</text>
</g>
</svg>
text-anchor se hereda, así que ponerlo en un g afecta a todos los textos de dentro. Es lo que se hace en un eje: todas las etiquetas del eje vertical con text-anchor="end" y la misma x, y quedan alineadas por la derecha sin calcular anchuras.
Nótese que start y end dependen de la dirección del texto, no del eje. En un texto de derecha a izquierda, start es el borde derecho. Eso es correcto y es la razón de que no se llamen left y right.
dominant-baseline y su historia
dominant-baseline hace en vertical lo que text-anchor en horizontal: elige qué línea base de la fuente se alinea con la y. Los valores que interesan:
| Valor | Alinea |
|---|---|
auto / alphabetic |
La línea base alfabética. Es el valor por omisión |
middle |
El punto medio de la altura de x |
central |
El centro del cuadratín (em box) |
hanging |
La base colgante, usada en escrituras índicas |
text-top / text-bottom |
Los bordes de la caja de texto |
Para centrar verticalmente un texto en una caja, los candidatos son middle y central. La diferencia: middle se calcula sobre la altura de x, que es la altura de una x minúscula y varía mucho entre fuentes; central se calcula sobre el cuadratín, que es una medida estable. En un texto en mayúsculas o en cifras, central centra mejor.
Y aquí está la advertencia práctica. dominant-baseline ha tenido implementaciones divergentes entre motores durante años, sobre todo en su interacción con alignment-baseline y con los tspan anidados. Los motores actuales han convergido bastante, pero si necesitas que un número quede centrado en un círculo con precisión de subpíxel en todos los navegadores, hay una alternativa que siempre funciona:
<text x="50" y="50" text-anchor="middle" dy="0.32em" font-size="16">42</text>
Ese dy="0.32em" desplaza la línea base hacia abajo un 32 por ciento del tamaño de fuente, que es aproximadamente la mitad de la altura de una mayúscula en la mayoría de fuentes. El texto queda centrado sobre la y declarada.
De dónde sale el 0,32: la altura de mayúscula típica ronda 0,70 em, así que su mitad es 0,35; restando la parte que ya queda por encima de la base, el ajuste práctico cae entre 0,30 y 0,35. Es un número empírico y es el que usan las bibliotecas de gráficos precisamente porque no depende de dominant-baseline.
text en SVG no hace saltos de línea automáticos. Ni por ancho, ni por salto de línea en el código fuente, ni con white-space: pre-wrap. Un texto de trescientos caracteres se dibuja en una sola línea que sale del lienzo por la derecha y se recorta contra el viewport.
SVG 2 especificó el ajuste automático con la propiedad inline-size sobre text, y su implementación lleva años sin ser universal. No cuentes con ella.
Eso deja tres caminos reales, y elegir mal cuesta caro.
Uno: componer las líneas a mano con varios tspan. Funciona en todas partes, se exporta bien, y obliga a medir el texto tú. Es lo que hace toda biblioteca de gráficos que pone etiquetas largas.
Dos: foreignObject con HTML dentro. El motor de maquetación de HTML hace el trabajo, con ajuste, guionado, columnas y todo. La contrapartida es dura: foreignObject no se renderiza cuando el SVG se sirve como imagen (img, background-image), no se exporta a PNG por la vía de dibujar el SVG en un lienzo, y muchas bibliotecas de renderizado en servidor no lo soportan. Es una solución excelente para SVG en línea en una página, y una trampa para todo lo demás.
Tres: no meter el texto en el SVG. Superponer un elemento HTML posicionado encima del SVG. Es lo que hacen las bibliotecas de gráficos serias para los tooltips y para las etiquetas largas, y es la respuesta correcta más veces de lo que parece: el texto queda seleccionable, accesible, ajustable y estilable con todo el poder de CSS, y el SVG se queda con lo que sabe hacer, que es la geometría.
La regla que se deduce: si el texto es corto y forma parte del gráfico (una etiqueta de eje, un valor), va en el SVG. Si es un párrafo, va en HTML encima.
Listas de coordenadas
x, y, dx, dy y rotate aceptan una lista de valores que se aplican carácter a carácter:
<text x="10 30 55 85 120" y="40" font-size="20">SVG!</text>
Cada carácter se coloca en su coordenada. Si hay menos valores que caracteres, los sobrantes siguen la disposición normal a partir del último posicionado. rotate es la excepción: su último valor se aplica a todos los caracteres restantes.
<!-- Todos los caracteres a 15 grados -->
<text x="10" y="40" rotate="15" font-size="20">Inclinado</text>
Esto sirve para efectos de texto disperso y, sobre todo, para animar cada letra por separado, que es el uso real: se generan los x desde código y se interpolan.
Un detalle: el posicionamiento por carácter opera sobre caracteres, no sobre grafemas ni sobre glifos. Un emoji compuesto de varios puntos de código o una letra con tilde descompuesta se descoordinan. Para texto latino sencillo funciona; para texto arbitrario, no.
Escribe una función que dada una cadena, un ancho máximo y un tamaño de fuente devuelva las líneas ya cortadas, usando getComputedTextLength() de un text de medición fuera de pantalla. Después compara el resultado con lo que produce un foreignObject con el mismo ancho, y anota las diferencias en el punto de corte. Sabrás entonces qué precisión estás perdiendo al componer a mano.