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.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
/**
* 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.
npx shadcn@latest add https://www.gassierconsulting.com/r/ai-typing-cursorEjemplo 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.
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.
| Propiedad | Tipo | Descripción |
|---|---|---|
text? | string | Opcional. |
speed? | number | Milisegundos por carácter. |
delay? | number | Espera antes de empezar, en milisegundos. |
cursor? | "bar" | "block" | "underline" | Opcional. |
font? | "sans" | "mono" | Opcional. |
loop? | boolean | Repite la escritura al terminar. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Variantes disponibles
cursor:bar,block,underlinefont: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.
| Variable | Valor 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".
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