wandres.dev
TEXTO EN SVG · text, tspan y textPath

textPath: texto sobre una ruta

Cómo se proyecta el texto sobre una curva, startOffset y su relación con pathLength, el lado de la ruta, y por qué el texto en un arco cerrado sale del revés en la mitad inferior.

⏱ 16 min

textPath toma un text y lo apoya sobre la geometría de una ruta, orientando cada glifo según la tangente. Es el efecto que da los sellos circulares, las etiquetas que siguen un río en un mapa y los rótulos curvos de un diagrama. Su modelo es sencillo y tiene tres detalles que hay que conocer antes de usarlo en serio: dónde empieza, hacia dónde miran las letras, y qué pasa con lo que no cabe.

🎯 Al terminar esta lección sabrás
  • Apoyar un texto sobre una ruta y controlar el punto de inicio.
  • Usar startOffset en unidades y en porcentaje, y combinarlo con pathLength.
  • Explicar por qué el texto se invierte en la parte inferior de un círculo y arreglarlo.
  • Anticipar qué ocurre con el texto que excede la longitud de la ruta.

La proyección

textPath va dentro de un text y referencia una ruta con href:

<svg viewBox="0 0 300 120" width="600">
  <defs>
    <path id="onda" d="M 20 80 Q 90 20 150 70 T 280 60" fill="none" />
  </defs>
  <path d="M 20 80 Q 90 20 150 70 T 280 60" fill="none"
        stroke="#45475a" stroke-dasharray="3 3" />
  <text font-size="20" fill="#cdd6f4" font-family="system-ui, sans-serif">
    <textPath href="#onda">Texto que sigue la curva</textPath>
  </text>
</svg>

Fíjate en tres cosas del marcado.

La ruta de referencia está en defs y no se pinta. La copia visible es otro elemento con la misma d. Si quisieras que la ruta se viera, podrías dejarla fuera de defs con un stroke, y el textPath la referenciaría igual.

El text no lleva x ni y: la posición la da la ruta. Si pones x e y, se ignoran para la componente que la ruta controla.

El texto se coloca recorriendo la ruta desde su inicio. Cada glifo se sitúa en el punto correspondiente a su avance acumulado y se rota según la tangente en ese punto. Esa rotación es lo que hace que el texto parezca doblarse.

La ruta puede ser cualquiera: una recta (lo que equivale a un texto normal rotado), un arco, una espiral. Solo tiene que ser un elemento de geometría al que se pueda apuntar.

startOffset

startOffset decide dónde, a lo largo de la ruta, empieza el texto. Acepta una longitud en unidades de usuario o un porcentaje de la longitud total de la ruta.

<textPath href="#onda" startOffset="50%">Centrado</textPath>

Combinado con text-anchor="middle" en el text, el startOffset pasa a ser el punto medio del texto, no su inicio. Esa es la combinación para centrar un rótulo en un arco:

<text font-size="18" text-anchor="middle">
  <textPath href="#arco" startOffset="50%">Centrado en el arco</textPath>
</text>

Y aquí conecta con algo del nivel 17: si la ruta lleva pathLength="100", el startOffset en unidades pasa a estar en esa escala ficticia, así que startOffset="25" es el 25 por ciento sin escribir el símbolo. Es útil cuando generas las posiciones desde datos y prefieres trabajar con números.

Un startOffset negativo o mayor que la longitud coloca el texto fuera de la ruta, donde no se dibuja. No es un error, simplemente no se ve.

El texto del revés

El caso clásico: un sello circular con texto arriba y abajo. El texto de arriba sale bien; el de abajo sale invertido y leyéndose de derecha a izquierda desde dentro del círculo.

No es un fallo: el texto sigue la tangente, y en la mitad inferior de un círculo recorrido en sentido horario la tangente apunta hacia la izquierda. Las letras están correctamente orientadas respecto a la curva; lo que pasa es que la curva va hacia allá.

Hay dos soluciones.

Una: usar dos rutas con sentidos opuestos. La de arriba en sentido horario, la de abajo en antihorario. Se consigue cambiando el sweep-flag del arco:

<defs>
  <!-- Arco superior, horario -->
  <path id="arriba" fill="none"
        d="M 30 100 A 70 70 0 0 1 170 100" />
  <!-- Arco inferior, antihorario, para que el texto salga derecho -->
  <path id="abajo" fill="none"
        d="M 30 100 A 70 70 0 0 0 170 100" />
</defs>

<text font-size="16" text-anchor="middle" fill="#cdd6f4">
  <textPath href="#arriba" startOffset="50%">TEXTO SUPERIOR</textPath>
</text>
<text font-size="16" text-anchor="middle" fill="#cdd6f4">
  <textPath href="#abajo" startOffset="50%">TEXTO INFERIOR</textPath>
</text>

Dos: usar el atributo side. SVG 2 añadió side="right" a textPath, que coloca el texto al otro lado de la ruta y con la orientación invertida. Es más elegante y su soporte no es universal; compruébalo antes de depender de él. La solución de las dos rutas funciona en todas partes.

Un tercer detalle del sello: el texto queda pegado a la ruta por su línea base, así que en el arco superior las letras van por fuera y en el inferior por dentro. Para desplazarlo hacia fuera o hacia dentro se usa un dy en el text:

<text font-size="16" text-anchor="middle" dy="-6">
  <textPath href="#abajo" startOffset="50%">TEXTO INFERIOR</textPath>
</text>

Lo que no cabe

Si el texto es más largo que la ruta, el exceso no se dibuja. Se corta en seco, sin puntos suspensivos y sin aviso. Es un comportamiento distinto del de un texto normal, que sí se sale del lienzo y se recorta contra el viewport: aquí desaparece antes.

Eso convierte a textPath en un elemento delicado con contenido dinámico. Un rótulo curvo con un nombre de producto puede verse completo con un producto y cortado con otro.

La defensa es medir. getComputedTextLength() sobre el text devuelve la longitud del texto, y getTotalLength() sobre la ruta devuelve la de la ruta. Si el primero excede al segundo, hay que reducir el tamaño de fuente o acortar el texto:

function ajustarATrayecto(textEl, rutaEl, tamMax = 18, tamMin = 9) {
  const largoRuta = rutaEl.getTotalLength();
  for (let t = tamMax; t >= tamMin; t -= 0.5) {
    textEl.style.fontSize = `${t}px`;
    if (textEl.getComputedTextLength() <= largoRuta) return t;
  }
  return tamMin;
}

El bucle es feo y funciona. La alternativa elegante es textLength, que se ve en la lección siguiente, con sus propias contrapartidas.

La rotación por glifo se calcula en el punto medio, y por eso las curvas cerradas separan las letras

Cada glifo se coloca según su avance acumulado a lo largo de la ruta y se rota según la tangente en el punto medio de ese glifo. Es una decisión razonable y produce dos artefactos en cuanto la curvatura es fuerte respecto al tamaño del texto.

El primero: en el interior de una curva cerrada, los glifos se solapan. Sus cajas se abanican hacia el centro y las bases se juntan más de lo que están sus centros. En el exterior ocurre lo contrario y las letras parecen separarse. Cuanto menor sea el radio en relación con el tamaño de fuente, más evidente.

El segundo: en un punto de curvatura discontinua (una esquina de la ruta, la unión de dos comandos sin continuidad de tangente) hay un salto de orientación entre dos letras consecutivas que se ve como un tropiezo.

Los dos tienen la misma solución de fondo: el radio de la curva debe ser grande respecto al tamaño de la fuente. Como regla, un radio menor que unas cinco veces el tamaño de fuente empieza a verse mal, y menor que tres veces es inutilizable. Un texto de 16 unidades sobre un círculo de radio 40 se ve apretado por dentro; sobre uno de radio 100, bien.

Y la segunda regla, más práctica: usa rutas con continuidad de tangente. Una d construida con arcos que encadenan bien, o con curvas suavizadas mediante S y T, evita los saltos. Una ruta hecha de segmentos rectos unidos en ángulo produce texto que da tumbos, y ningún ajuste de tamaño lo arregla.

⚔️ Reto práctico

Construye un sello circular con texto arriba, texto abajo y un icono en el centro, todo dentro de un único svg de 200 por 200. Comprueba las cuatro cosas: que el texto inferior se lee del derecho, que ambos están centrados con startOffset="50%" y text-anchor="middle", que el desplazamiento hacia fuera es simétrico, y que un texto demasiado largo no se corta porque lo has medido antes.