El efecto de máquina de escribir es el equivalente digital de ver a alguien pensar en voz alta. El texto aparece carácter por carácter, como si una mano real estuviera pulsando teclas reales. Lo ves en las secciones hero de sitios de portafolio, en emuladores de terminal basados en el navegador y en las ventanas de chat de asistentes de IA que quieren demostrar que están "escribiendo" una respuesta en lugar de simplemente recuperar un bloque de texto prefabricado. Cuando se hace bien, crea anticipación. Cuando se hace mal, parece una impresora atascada en 1987.

Por qué persiste este patrón

Las computadoras entregan información al instante. Los humanos no. La brecha entre esas dos velocidades es útil. El efecto de máquina de escribir la cierra simulando el ritmo humano. En una landing page, puede atraer la mirada a través de un titular palabra por palabra para que los visitantes realmente lean la propuesta de valor en lugar de solo echar un vistazo rápido. En un emulador de terminal, vende la ilusión de que los comandos se están ejecutando en tiempo real. En una interfaz de chatbot, el ritmo indica que se está generando una respuesta sobre la marcha en lugar de recuperarla de una base de datos.

Pero el efecto solo funciona si la mecánica respeta al usuario. Un tic-tic-tic plano y metronómico de intervalos idénticos se siente robótico. Peor aún, una implementación que ignore la tecnología de asistencia puede convertir un detalle visual divertido en una barrera frustrante. El objetivo no es ralentizar al usuario; es añadir la fricción justa para que la interfaz se sienta viva.

Constrúyelo con setTimeout recursivo

Empieza con setTimeout y no toques setInterval. La diferencia importa más de lo que parece.

setInterval es obstinado. Se ejecuta cada n milisegundos independientemente de lo que esté sucediendo en tu script o en el hilo principal del navegador. Si tu lógica necesita una pausa de 50 milisegundos entre pulsaciones normales pero una pausa de 150 milisegundos después de la puntuación, setInterval no puede adaptarse. Terminas envolviéndolo en lógica condicional extra, luchando contra condiciones de carrera y, finalmente, limpiando y reiniciando el intervalo tan a menudo que el código se convierte en una pesadilla de gestión de estados. Los intervalos fijos fallan en el momento en que necesitas velocidades variables.

El setTimeout recursivo soluciona esto permitiendo que cada paso decida las reglas para el siguiente. Piensa en ello como una pequeña máquina de estados. Mantienes algunas variables: la cadena actual, el índice del carácter actual, un flag booleano para saber si estás escribiendo o borrando, y un contador textIndex para poder iterar a través de múltiples cadenas. La función añade un carácter, comprueba en qué parte de la frase se encuentra y luego programa su propia siguiente invocación con un retraso que coincida con el contexto.

Por ejemplo, podrías escribir la mayoría de los caracteres a 50 milisegundos, ralentizar a 150 milisegundos después de una coma y hacer una pausa de 800 milisegundos al final de una oración completa antes de cambiar al modo de borrado. No puedes hacer eso de forma limpia con setInterval. Con setTimeout recursivo, la lógica es sencilla:

if typing:
  append next character
  if at end of string:
    switch to pause mode
    schedule next call after 1000ms
if deleting:
  remove last character
  if string empty:
    increment textIndex
    load next string
    switch to typing mode

Esta estructura también hace que la limpieza sea trivial. Guarda el ID del timeout. Cuando el componente se desmonte o el usuario navegue a otra parte, llama a clearTimeout una vez. Sin intervalos huérfanos funcionando en segundo plano.

El cursor debe parpadear por su cuenta

El cursor parpadeante es un detalle visual, no una preocupación de datos. Manténlo fuera de tu motor de estado de JavaScript. Usa una animación CSS separada adjunta a un pseudo-elemento ::after o un <span dedicado al final de tu contenedor de texto.

Un simple @keyframes blink que alterne la opacity o el border-color con un timing de step-end te ofrece un pulso nítido y amigable con el hardware que se ejecuta en el compositor. JavaScript no tiene por qué microgestionar la visibilidad del cursor. Si alternas propiedades de visualización desde dentro de tu recursión de setTimeout, obligas a realizar recálculos de estilo innecesarios con cada carácter. Deja que CSS se encargue de la estética. Deja que JavaScript se encargue de la secuencia.

Iterar a través de múltiples cadenas es sencillo con un contador textIndex. Guarda tus cadenas en un array. Cuando la animación termine su fase de borrado y el contenedor esté vacío, incrementa el textIndex usando el módulo de la longitud del array, reinicia el puntero de caracteres a cero y comienza a escribir de nuevo. Así es como los sitios de portafolio ciclan entre roles —["Desarrollador", "Diseñador", "Escritor"]— sin necesidad de recargar la página.

Errores que rompen la ilusión

Tres errores aparecen una y otra vez en las implementaciones de aficionados.

Uso de setInterval. Ya hemos cubierto el problema de la velocidad variable, pero hay un problema más sutil. Si la actualización de tu DOM llega a retrasarse —por ejemplo, porque el navegador está pintando un cambio de diseño— setInterval sigue ejecutándose. Puedes terminar con escrituras superpuestas, caracteres duplicados o escrituras que ocurren más rápido de lo que el navegador puede renderizarlas. Un setTimeout recursivo espera hasta que el paso actual ha terminado antes de siquiera pensar en el siguiente.

Olvidar escapar el HTML. Si tus cadenas de origen contienen paréntesis angulares y estás inyectando contenido a través de innerHTML carácter por carácter, dividirás las etiquetas por la mitad. El navegador ve <, luego <s, luego <st. Eso impide el análisis correcto de las etiquetas y puede dejarte con nodos del DOM rotos o cascadas de estilos inesperadas. Si quieres que aparezcan los caracteres literales, escápalos primero o, mejor aún, escribe en textContent en lugar de innerHTML. Si realmente necesitas <span> con estilos dentro del resultado de la máquina de escribir, preprocesa la cadena para saber exactamente dónde empiezan y terminan las etiquetas antes de comenzar el bucle de caracteres.

Ignorar la accesibilidad. A los lectores de pantalla no les gusta que se les lea una letra a la vez. A medida que tu script añade cada nuevo carácter al DOM, algunas tecnologías de asistencia anuncian el nodo completo de nuevo, provocando una ráfaga entrecortada de palabras parciales. Eso es una pesadilla para cualquiera que dependa de la navegación auditiva. La solución no es complicada: añade un aria-label al contenedor que contiene el texto completo y final. También puedes ocultar el elemento animado por completo de la tecnología de asistencia con aria-hidden="true" y proporcionar una copia estática visualmente oculta para los lectores de pantalla. De cualquier manera, ofrece a los usuarios la frase completa desde el principio en lugar de obligarlos a presenciar tu actuación.

Formas de pulir tu versión

Una vez que el bucle principal funcione correctamente, puedes añadir extras. Pero resiste la tentación de agregarlos hasta que los fundamentos sean sólidos.

Efectos de sonido de pulsación de teclas. Un clic sutil en cada carácter puede ser satisfactorio, pero el audio en las páginas web es un campo minado. Usa la Web Audio API o un elemento Audio ligero con un búfer corto. Varía ligeramente la velocidad de reproducción —entre 0.95 y 1.05— para que los clics idénticos no suenen sintéticos. Respeta siempre las políticas de reproducción automática del navegador y proporciona un interruptor de silencio. Nada ahuyenta más rápido a los usuarios que una página de portafolio sin silenciar que reproduzca automáticamente sonidos de escritura a las 9 AM.

Escritura multilínea. Las ventanas de terminal reales tienen ajuste de línea. Si tu texto cruza un salto de línea, un cursor simple con border-right saltará de forma extraña a menos que tu diseño sea predecible. Divide las cadenas por caracteres de nueva línea y renderiza cada línea en su propio <span>, o utiliza un pseudo-elemento posicionado que rastree el final del contenido. Ten cuidado con el ajuste de texto; un cursor implementado como un borde en línea puede desprenderse del texto si el ancho del contenedor cambia. Considera usar white-space: pre-wrap y una fuente monoespaciada para estilos de terminal, ya que los caracteres de ancho fijo hacen que el cálculo del cursor sea mucho más predecible.

Renderizado de Markdown en tiempo real. Aquí es donde las cosas se complican. Si escribes **bold**, tienes una opción: renderizar los asteriscos literalmente tal como aparecen, o convertirlos a un estilo de negrita sobre la marcha. Si eliges lo segundo, cambiar de textContent a innerHTML a mitad del proceso significa que los límites de tus nodos de texto cambian. La posición del cursor se convierte en un dolor de cabeza de gestión porque las etiquetas HTML desplazan el árbol del DOM debajo de ti. Un enfoque más seguro es escribir la cadena de Markdown sin procesar normalmente y luego activar una pasada de renderizado una vez que la cadena completa esté en pantalla. Si realmente necesitas formato en vivo, mantén dos capas: un búfer de escritura oculto y una superposición visual procesada.

Efectos inversos. Borrar texto no tiene por qué significar retroceder un carácter a la vez. Puedes simular un reinicio de "seleccionar todo, luego borrar" que limpie el campo instantáneamente antes de que se escriba la siguiente cadena. Eso se siente clínico. Alternativamente, un borrado lento a 30 milisegundos por carácter genera tensión. Mezcla ambos: borra un error tipográfico rápidamente, haz una pausa y luego reanuda el borrado a velocidad normal. La variación le da ese toque humano al efecto.

La verdadera conclusión

Un efecto de máquina de escribir es uno de esos adornos de la interfaz de usuario que parece trivial en la superficie y revela su complejidad solo después de haberlo construido. Empieza con