Ir al contenido
Evaluar mi proyecto
Componentes · IADisponible

Texto que se escribe

Texto que se teclea con ritmo humano y un cursor de barra, bloque o subrayado.

Revisé los pedidos de esta semana: tres llevan más de cinco días de retraso.

Demostración interactiva

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

ai-typing-cursor.tsx102 líneas
Descargar
/**
 * Texto que se escribe solo, con un ritmo humano: pausas más largas en comas y puntos y un
 * ligero desfase entre letras. El cursor (barra, bloque o subrayado) queda fijo mientras
 * escribe y parpadea cuando termina. Lector de pantalla: recibe el texto completo desde el
 * inicio. Los colores se sobrescriben con --gcai-ink y --gcai-accent.
 *
 *   <AiTypingCursor
 *     text="Revisé los pedidos de la semana."
 *     speed={34}
 *     cursor="block"
 *     font="mono"
 *   />
 */

"use client";

import { useEffect, useState, type CSSProperties } from "react";

export type AiTypingCursorProps = {
  text?: string;
  /** Milisegundos por carácter. */
  speed?: number;
  /** Espera antes de empezar, en milisegundos. */
  delay?: number;
  cursor?: "bar" | "block" | "underline";
  font?: "sans" | "mono";
  /** Repite la escritura al terminar. */
  loop?: boolean;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gctp-root { --gctp-ink: var(--gcai-ink, #ededee); --gctp-accent: var(--gcai-accent, #84b3ff); display: block; box-sizing: border-box; width: min(100%, 720px); margin-inline: auto; color: var(--gctp-ink); font: 500 clamp(20px, 2.6vw, 26px)/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; letter-spacing: -.015em; overflow-wrap: anywhere; text-wrap: pretty; }
.gctp-root[data-font="mono"] { font: 400 clamp(16px, 2vw, 19px)/1.7 ui-monospace, "SF Mono", Menlo, Consolas, monospace; letter-spacing: 0; }
.gctp-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.gctp-cursor { display: inline-block; margin-left: 2px; background: var(--gctp-accent); vertical-align: text-bottom; animation: gctp-blink 1.1s steps(1, end) infinite; }
.gctp-cursor[data-typing="true"] { animation: none; }
.gctp-cursor[data-shape="bar"] { width: 2px; height: 1.1em; }
.gctp-cursor[data-shape="block"] { width: .55em; height: 1.1em; }
.gctp-cursor[data-shape="underline"] { width: .6em; height: 2px; margin-bottom: .12em; }
@keyframes gctp-blink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .gctp-cursor { animation: none; } }
`;

export function AiTypingCursor({
  text = "Revisé los pedidos de esta semana: tres llevan más de cinco días de retraso.",
  speed = 34,
  delay = 400,
  cursor = "bar",
  font = "sans",
  loop = false,
  className,
  style,
}: AiTypingCursorProps) {
  const [reduced, setReduced] = useState(false);
  const [progress, setProgress] = useState({ cycle: -1, shown: 0 });
  const [cycle, setCycle] = useState(0);
  const shown = reduced ? text.length : progress.cycle === cycle ? progress.shown : 0;

  useEffect(() => {
    const media = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setReduced(media.matches);
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);

  useEffect(() => {
    if (reduced) return;
    let timer: ReturnType<typeof setTimeout>;
    let index = 0;
    const step = () => {
      index += 1;
      setProgress({ cycle, shown: index });
      if (index >= text.length) {
        if (loop) timer = setTimeout(() => setCycle((n) => n + 1), 2400);
        return;
      }
      const previous = text[index - 1];
      const pause = /[.!?]/.test(previous) ? 11 : /[,;:]/.test(previous) ? 6 : 1;
      const jitter = 0.7 + ((index * 13) % 7) * 0.09;
      timer = setTimeout(step, Math.max(8, speed) * pause * jitter);
    };
    timer = setTimeout(step, Math.max(0, delay) + (cycle > 0 ? 500 : 0));
    return () => clearTimeout(timer);
  }, [text, speed, delay, reduced, loop, cycle]);

  return (
    <>
      <style>{CSS}</style>
      <p className={["gctp-root", className].filter(Boolean).join(" ")} style={style} data-font={font}>
        <span className="gctp-sr">{text}</span>
        <span aria-hidden="true">
          {text.slice(0, shown)}
          <span className="gctp-cursor" data-shape={cursor} data-typing={shown > 0 && shown < text.length} />
        </span>
      </p>
    </>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/ai-typing-cursor

Ejemplo de uso de Texto que se escribe

Después de copiar ai-typing-cursor.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { AiTypingCursor } from "./ai-typing-cursor";

export default function Ejemplo() {
  return <AiTypingCursor cursor="bar" text="Ejemplo" loop />;
}

Propiedades de AiTypingCursor

AiTypingCursor acepta 8 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
text?stringOpcional.
speed?numberMilisegundos por carácter.
delay?numberEspera antes de empezar, en milisegundos.
cursor?"bar" | "block" | "underline"Opcional.
font?"sans" | "mono"Opcional.
loop?booleanRepite la escritura al terminar.
className?stringOpcional.
style?CSSPropertiesOpcional.

Variantes disponibles

  • cursor: bar, block, underline
  • font: sans, mono

Personaliza Texto que se escribe con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--gcai-ink#ededee
--gcai-accent#84b3ff

Accesibilidad y rendimiento

El archivo tiene 102 líneas (4 KB) y define 1 animación con @keyframes. Esto es lo que hace Texto que se escribe por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Expone su estado a tecnologías de apoyo con atributos aria-*.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18

Acerca del producto

Las pausas son más largas en comas y puntos y cada letra llega con un desfase ligero. El cursor queda fijo mientras escribe y parpadea al terminar. El lector de pantalla recibe el texto completo.

Incluye

Un solo archivo React + TypeScript con estilos dentro. Sin Tailwind ni dependencias externas.

  • Pausas en puntuación
  • Tres formas de cursor
  • Tipografía sans o monoespaciada
  • Opción de repetir
  • Respeta prefers-reduced-motion