wandres.dev
TEXTO EN CANVAS · Medir y componer

textAlign y textBaseline: qué significa cada línea

Controlar el punto de anclaje del texto en los dos ejes, entender las líneas base tipográficas y elegir la correcta para cada caso.

⏱ 16 min

Las coordenadas de fillText no señalan la esquina del texto: señalan un punto de anclaje cuyo significado lo deciden dos propiedades. textAlign decide qué representa la X, y textBaseline decide qué representa la Y. La segunda tiene seis valores que corresponden a líneas tipográficas reales, y elegir mal entre ellas es la causa de que las etiquetas de un gráfico nunca acaben de estar centradas.

🎯 Al terminar esta lección sabrás
  • Usar los cinco valores de textAlign incluidos los sensibles a la dirección.
  • Describir las líneas base disponibles y decir cuál corresponde a cada tipo de escritura.
  • Elegir la línea base correcta para centrar texto verticalmente.
  • Explicar por qué el centrado con middle no siempre parece centrado.

textAlign: el eje horizontal

Cinco valores:

  • start (por defecto): el borde inicial según la dirección del texto. Con dirección de izquierda a derecha equivale a left.
  • end: el borde final. Con esa misma dirección equivale a right.
  • left y right: absolutos, ignoran la dirección.
  • center: el punto es el centro horizontal del texto.

La distinción entre start/end y left/right importa en interfaces multilingües. Con ctx.direction = 'rtl', un textAlign = 'start' alinea a la derecha, que es lo correcto para árabe o hebreo. left seguiría alineando a la izquierda.

ctx.font = '20px system-ui';
ctx.fillStyle = '#89b4fa';
ctx.strokeStyle = '#f38ba8'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(200.5, 10); ctx.lineTo(200.5, 130); ctx.stroke();

['left', 'center', 'right'].forEach((a, i) => {
  ctx.textAlign = a;
  ctx.fillText(`texto ${a}`, 200, 40 + i * 34);
});

textAlign es la forma correcta de centrar texto. La alternativa —medir el ancho y restar la mitad— funciona, es más código y da resultados ligeramente distintos porque el ancho medido no siempre coincide con el ancho de avance que usa el motor de alineación.

textBaseline: las líneas

Aquí hay más sustancia, porque las líneas base no son invención del canvas sino conceptos tipográficos con significado real.

alphabetic (por defecto): la línea sobre la que se apoyan las letras latinas, griegas y cirílicas. Es la línea base de verdad en la tipografía occidental. Las partes descendentes caen por debajo.

ideographic: la línea base de los ideogramas del este asiático, que se apoyan un poco más abajo que la alfabética.

hanging: la línea base colgante, usada en escrituras índicas como el devanagari, donde los caracteres cuelgan de una línea superior.

top: el borde superior de la caja de la fuente, definida por las métricas del fichero de fuente.

bottom: el borde inferior de esa misma caja.

middle: el punto medio de la caja em, aproximadamente a media altura de la x, no a la mitad de la caja completa.

const lineas = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom'];
ctx.font = '28px Georgia, serif';
lineas.forEach((base, i) => {
  const y = 40 + i * 44;
  ctx.strokeStyle = '#f38ba8'; ctx.lineWidth = 1;
  ctx.beginPath(); ctx.moveTo(20, y + 0.5); ctx.lineTo(400, y + 0.5); ctx.stroke();
  ctx.textBaseline = base;
  ctx.fillStyle = '#cdd6f4';
  ctx.fillText('Hpgjéx 漢字', 30, y);
});

Ejecutar ese fragmento y ver dónde cae la línea roja respecto al texto vale más que cualquier descripción.

Centrar verticalmente: el problema real

La tarea más común es centrar texto dentro de una caja, y la solución obvia —textBaseline = 'middle' con la Y en el centro de la caja— es la correcta la mayoría de las veces y produce un resultado ligeramente alto en algunos casos.

La razón es que middle se define respecto a la caja de la fuente, que reserva espacio para los ascendentes y descendentes máximos que la fuente pueda tener, no respecto al texto concreto que estás dibujando. Una cadena sin descendentes parece desplazada hacia arriba, porque se ha reservado espacio debajo para descendentes que no existen.

Cuando el centrado tiene que ser óptico y no métrico, hay que usar las métricas reales del texto:

function centrarOptico(ctx, texto, cx, cy) {
  const m = ctx.measureText(texto);
  const alto = m.actualBoundingBoxAscent + m.actualBoundingBoxDescent;
  const y = cy + alto / 2 - m.actualBoundingBoxDescent;
  ctx.textAlign = 'center';
  ctx.textBaseline = 'alphabetic';
  ctx.fillText(texto, cx, y);
}

Esa versión centra la tinta real. Tiene una contrapartida importante: el resultado depende del contenido, así que dos etiquetas en la misma fila con contenidos distintos quedarán a alturas ligeramente distintas. Para etiquetas de una tabla o de un eje, eso es peor que el desajuste que corrige.

Situación Estrategia
Etiquetas en fila, contenido variable middle, métrico, consistente entre etiquetas
Un texto centrado en un botón o círculo middle está bien; óptico si se nota
Un título grande y aislado Óptico, la diferencia se ve
Texto sobre una línea base compartida alphabetic, siempre
La línea base es lo único que alinea texto de tamaños distintos, y middle lo estropea

Hay una situación en la que elegir middle no es una cuestión de gusto sino un error: cuando hay varios textos de tamaños distintos que deben verse alineados en la misma fila. Piensa en una cifra grande con su unidad pequeña al lado, en un título con un superíndice, o en un eje donde el valor va en un cuerpo y el sufijo en otro. Si alineas ambos por middle, cada uno se centra respecto a su propia caja de fuente, que es proporcional a su tamaño, y el resultado es que los dos quedan visualmente desalineados: el pequeño flota respecto al grande. Si los alineas por alphabetic, ambos se apoyan sobre la misma recta, que es exactamente lo que hace la tipografía desde hace quinientos años y lo que el ojo espera. La línea base existe precisamente para eso: es el invariante que permite mezclar tamaños, pesos y familias en una misma línea y que todo se vea sobre el mismo renglón. Es también la razón de que sea el valor por defecto del canvas, decisión que a mucha gente le parece incómoda hasta que entiende para qué está. La regla que resume todo esto: alphabetic para texto que convive con otro texto; middle para texto que convive con una caja. Y un corolario práctico: cuando dibujes una fila con varios fragmentos, calcula una sola coordenada Y de línea base para toda la fila y dibuja todos los fragmentos con esa Y, cambiando solo ctx.font entre ellos. Si te ves calculando una Y por fragmento, estás a punto de introducir el desajuste.

Un ejemplo con varios tamaños

<!doctype html>
<meta charset="utf-8">
<canvas id="c" width="560" height="220" style="background:#1e1e2e"></canvas>
<script>
  const ctx = document.getElementById('c').getContext('2d');

  function metrica(x, yBase, valor, unidad, etiqueta) {
    ctx.textBaseline = 'alphabetic';
    ctx.textAlign = 'left';

    ctx.font = '700 52px system-ui';
    ctx.fillStyle = '#cdd6f4';
    ctx.fillText(valor, x, yBase);
    const anchoValor = ctx.measureText(valor).width;

    ctx.font = '400 20px system-ui';
    ctx.fillStyle = '#89b4fa';
    ctx.fillText(unidad, x + anchoValor + 6, yBase);

    ctx.font = '400 14px system-ui';
    ctx.fillStyle = '#7f849c';
    ctx.fillText(etiqueta, x, yBase + 24);
  }

  metrica(40, 90, '1.284', 'req/s', 'Peticiones por segundo');
  metrica(300, 90, '99,7', '%', 'Disponibilidad');

  ctx.strokeStyle = '#45475a';
  ctx.setLineDash([3, 3]);
  ctx.beginPath(); ctx.moveTo(20, 90.5); ctx.lineTo(540, 90.5); ctx.stroke();
  ctx.setLineDash([]);
</script>

La línea discontinua muestra el invariante: los cuatro fragmentos de texto, con dos tamaños y dos pesos, se apoyan sobre la misma recta. Cambiar cualquiera de ellos a middle rompería la alineación de inmediato.

Interacción con la dirección

Una última nota. ctx.direction afecta a start y end de textAlign, y también al orden visual de los caracteres en textos bidireccionales. El canvas aplica el algoritmo bidireccional de Unicode al dibujar, así que una cadena mixta de árabe y latín se reordena correctamente. Lo que no puede hacer es reordenar entre llamadas distintas: si dibujas la cadena en fragmentos, cada fragmento se reordena por separado y el resultado es incorrecto. Para texto bidireccional, dibuja la cadena completa en una sola llamada.