Volver a las notas

Intl.Segmenter: divide texto sin romper los emojis

1 de noviembre de 2026·
javascriptintlunicodetypography
Demo en vivoAbrir a pantalla completa

Si alguna vez has animado un titular letra a letra y has visto cómo un emoji se convertía en una fila de cuadraditos rotos, el problema nunca fue tu CSS: fue split(""). Intl.Segmenter divide el texto en las piezas que percibe quien lo lee, y desde el 16 de octubre de 2026 es Baseline Widely available.

Escribe lo que quieras en la demo de arriba. La fila de arriba es lo que devuelve text.split(""); la de abajo, Intl.Segmenter. Con un emoji de familia, una bandera y una palabra con tilde, la diferencia salta a la vista.

Por qué split("") te engaña

Las cadenas de JavaScript son secuencias de unidades de código UTF-16. length, split("") y el acceso por índice trabajan con esas unidades, no con caracteres tal y como los ve una persona:

const text = "👨‍👩‍👧‍👦 café";

text.length;            // 16
text.split("").length;  // 16 → mitades sueltas de pares sustitutos
[...text].length;       // 12 → puntos de código: la familia se desarma

Expandir la cadena ([...text]) es mejor, porque recorre puntos de código, pero sigue sin ser lo que quieres. Ese emoji de familia son siete puntos de código: cuatro personas unidas por tres caracteres de unión de ancho cero (ZWJ). Una bandera son dos símbolos indicadores regionales. Una tilde puede ser una marca combinante aparte, detrás de la letra. Nada de eso es un "carácter" para quien lee.

Qué hace Intl.Segmenter

Intl.Segmenter segmenta texto teniendo en cuenta el idioma. Lo creas con una configuración regional y una granularity, y su método segment() devuelve un objeto Segments iterable:

const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });

[...segmenter.segment(text)].map((s) => s.segment);
// ["👨‍👩‍👧‍👦", " ", "c", "a", "f", "é"]

granularity admite tres valores:

  • "grapheme" (el valor por defecto): caracteres tal y como los percibe el usuario, los grupos de grafemas.
  • "word": límites de palabra, según el idioma.
  • "sentence": límites de frase.

Cada segmento es un objeto con segment (el texto), index (dónde empieza en la cadena original, en unidades de código) e input (la cadena completa). Con granularity: "word" además tienes isWordLike, que vale true para letras, números o ideogramas y false para espacios y signos de puntuación. Contar palabras se queda en una línea:

const words = new Intl.Segmenter("en", { granularity: "word" });
const countWords = (str) =>
  [...words.segment(str)].filter((s) => s.isWordLike).length;

countWords("Hello, world! It’s 2026."); // 4

El modo palabra es donde la API demuestra lo que vale. Partir por espacios no sirve de nada en idiomas que no separan las palabras con espacios, como el japonés, el chino o el tailandés. El ejemplo de MDN divide 吾輩は猫である。名前はたぬき。 en 吾輩 | は | 猫 | で | ある | 。 | 名前 | は | たぬき | 。 y cuenta 8 palabras. Pulsa el preset "Japonés" de la demo: split(" ") devuelve la frase entera como una sola pieza y el segmentador devuelve las palabras.

La animación letra a letra, bien hecha

Esto es el núcleo de la demo de arriba. Cada grafema va en su propio <span>, con su posición en una propiedad personalizada para que CSS escalone la animación:

const segmenter = new Intl.Segmenter(document.documentElement.lang || undefined, {
  granularity: "grapheme",
});

for (const el of document.querySelectorAll(".split")) {
  const pieces = [...segmenter.segment(el.textContent)].map(({ segment }, i) => {
    const span = document.createElement("span");
    span.textContent = segment;
    span.style.setProperty("--i", i);
    return span;
  });
  el.replaceChildren(...pieces);
}
.split span {
  display: inline-block;
  animation: pop 0.5s both;
  animation-delay: calc(var(--i) * 45ms);
}
@keyframes pop {
  from { opacity: 0; transform: translateY(0.5em) scale(0.6); }
}

El emoji de familia se queda en un solo span, la bandera sigue siendo una bandera y la letra con tilde conserva su tilde. La demo envuelve la misma idea en una comprobación de prefers-reduced-motion, para que la animación desaparezca si alguien ha pedido menos movimiento.

Encontrar el segmento en una posición

Segments tiene otro método que pasa fácilmente desapercibido: containing(index). Le das un índice en unidades de código y te devuelve el segmento que lo contiene, o undefined si el índice se sale de la cadena:

const sentence = "Find the word under the cursor.";
words.segment(sentence).containing(11);
// { segment: "word", index: 9, input: "...", isWordLike: true }

Es justo lo que necesitas para saber "qué palabra hay bajo el cursor" a partir del selectionStart de un input. En la demo, al tocar o enfocar una pieza de la fila de abajo se llama a containing() y se muestra el segmento, su index, su .length en unidades de código y los puntos de código que lleva dentro. El emoji de familia da un .length de 11.

Soporte y trampas

Según los datos de compatibilidad de MDN, Intl.Segmenter llegó en Chrome y Edge 87, Safari 14.1 (iOS 14.5) y Firefox 125, y en Node.js 16. web-features lo marca como Baseline Newly available el 16 de abril de 2024, cuando Firefox 125 completó el grupo, y Widely available el 16 de octubre de 2026. Widely available se define como 30 meses después de esa fecha clave.

Cosas a tener en cuenta:

  • index va en unidades de código. Es cómodo, porque text.slice(s.index, s.index + s.segment.length) te devuelve el segmento. Pero no lo mezcles con posiciones en puntos de código sacadas de [...text].
  • isWordLike solo existe en modo palabra. Con "grapheme" o "sentence" es undefined, no false.
  • Depende del idioma. Pasa el idioma del texto, no el de tu interfaz, cuando no coincidan. Si no pasas nada, se usa la configuración regional por defecto del entorno.
  • new es obligatorio. Llamar a Intl.Segmenter() sin él lanza un TypeError.
  • Plan B. Si todavía tienes que dar soporte a motores antiguos, comprueba "Segmenter" in Intl. La demo recurre a Array.from(), que al menos nunca parte un par sustituto, y muestra un aviso. MDN enlaza a un polyfill de FormatJS si necesitas el comportamiento real.

Qué vigilar

  • Tu propio código de texto partido. Si una animación letra a letra, un efecto de máquina de escribir o un componente de "revelado" de tu proyecto usa split("") o [...text], pruébalo con un emoji o con una tilde combinante. Ahora que la API es Widely available, cambiarlo por un segmentador de grafemas es un cambio pequeño.
  • Contadores de caracteres. Cualquier contador de "te quedan 280 caracteres" basado en .length cuenta unidades de código. Segmentar por grafemas te da una cifra más cercana a lo que ve el usuario.
  • El modo palabra fuera del inglés. Si tu contenido es multilingüe, los recuentos de palabras y las estimaciones de "tiempo de lectura" basadas en espacios fallan en algunos idiomas. isWordLike lo resuelve sin que tengas que mantener un diccionario.

Fuentes