Skip to Content

TypeAhead

A headless typewriter effect. Compose static and animated text inline; the whole tree is rendered as <span> elements, so it sits safely inside a <p> or any other text container. Cycles through one or many strings, with full control over typing speed, deletion speed, pause duration and the blinking caret. Honors prefers-reduced-motion by snapping to the final string.

I love building

import { TypeAhead } from "@togetheragency/ui/type-ahead"; export function BasicTypeAhead() { return ( <p className="text-2xl font-medium"> <TypeAhead.Root> <TypeAhead.Static>I love building </TypeAhead.Static> <TypeAhead.Animated texts={["React apps", "design systems", "tiny components"]} /> </TypeAhead.Root> </p> ); }

Import

import { TypeAhead } from "@togetheragency/ui/type-ahead";

The component is exported as a compound object; every sub-component is a property of TypeAhead:

TypeAhead.Root; TypeAhead.Static; TypeAhead.Animated;

Composition

TypeAhead.Root provides default animation settings to descendant TypeAhead.Animated parts via context. Mix and match Static and Animated segments freely:

<p> <TypeAhead.Root typingSpeed={28}> <TypeAhead.Static>I love </TypeAhead.Static> <TypeAhead.Animated texts={["React", "Vue", "Svelte"]} /> <TypeAhead.Static> for building UIs.</TypeAhead.Static> </TypeAhead.Root> </p>
  • TypeAhead.Root renders a <span> and seeds defaults for descendants. Because it’s an inline element, it can be nested inside <p>, headings, list items — anywhere text belongs.
  • TypeAhead.Static renders an inert <span> of text. Use it to weave static copy through an animated sentence without breaking the inline flow.
  • TypeAhead.Animated runs the typing state machine. Accepts a single string or an array of strings to cycle through. All Root settings can be overridden per instance.

Each TypeAhead.Animated runs an independent state machine and only re-renders itself on each frame — sibling Static parts and the surrounding Root are not affected by the animation tick.

Usage

Single string

Pass a string to texts to type a one-off message. Loop defaults to false for single strings, so the animation halts on the final character.

<TypeAhead.Root> <TypeAhead.Animated texts="Headless. Accessible. Tiny." loop={false} /> </TypeAhead.Root>

Cycling through strings

Pass an array of strings. Each entry is typed, paused, deleted, then the next entry types in. Looping defaults to true for arrays.

<TypeAhead.Animated texts={["alpha", "bravo", "charlie"]} />

Speed controls

Tune typingSpeed and deletionSpeed (both in characters per second) and pauseDuration (milliseconds) either on the Root for defaults or per-Animated for overrides.

Fast:

Slow:

<TypeAhead.Root typingSpeed={48} deletionSpeed={64} pauseDuration={800}> <TypeAhead.Animated texts={["snappy", "rapid", "instant"]} /> </TypeAhead.Root> <TypeAhead.Root typingSpeed={8} deletionSpeed={12} pauseDuration={2000}> <TypeAhead.Animated texts={["deliberate", "patient", "measured"]} /> </TypeAhead.Root>

Custom cursor

Swap the caret character or hide it entirely via cursorCharacter and showCursor. Useful for terminal-style aesthetics or quieter implementations.

>

SYSTEM://

No cursor —

<TypeAhead.Root cursorCharacter="▌"> <TypeAhead.Static>{"> "}</TypeAhead.Static> <TypeAhead.Animated texts={["whoami", "ls -la", "exit"]} /> </TypeAhead.Root> <TypeAhead.Root showCursor={false}> <TypeAhead.Static>No cursor — </TypeAhead.Static> <TypeAhead.Animated texts={["quiet typing", "no blink", "minimal"]} /> </TypeAhead.Root>

Listening to typing events

TypeAhead.Animated exposes onType, onDelete and onComplete callbacks. onType fires each time a string is fully typed, onDelete when it’s fully cleared, and onComplete once the animation halts (only meaningful when loop is false).

Currently showing:

Recently typed:
const [history, setHistory] = useState<string[]>([]); <TypeAhead.Root> <TypeAhead.Static>Currently showing: </TypeAhead.Static> <TypeAhead.Animated texts={["alpha", "bravo", "charlie", "delta"]} onType={(text) => setHistory((cur) => [text, ...cur].slice(0, 4))} /> </TypeAhead.Root>;

Custom render

Pass render to take over the output — receives the current displayed text, the full target string, the active index, the phase and a done flag. Useful for adding decorations (badges, scrubbers, custom carets) without forking the typing state machine.

Mood:

<TypeAhead.Animated texts={["focused", "playful", "shipping"]} render={({ text, phase }) => ( <span data-phase={phase} className="rounded-md bg-foreground/10 px-2 py-0.5 font-mono" > {text} <span aria-hidden="true" className="ml-0.5 animate-pulse"> </span> </span> )} />

Reduced motion

When the user has prefers-reduced-motion: reduce set, TypeAhead.Animated snaps directly to the final string and stops the caret blink. No additional configuration required — the component reads the media query via motion’s useReducedMotion hook on mount.

API Reference

TypeAhead.Root

Renders a <span> and seeds default animation settings for descendant TypeAhead.Animated parts via context. Every setting on Root can be overridden per-instance on Animated.

PropTypeDefaultDescription
typingSpeednumber24Default characters per second while typing.
deletionSpeednumber36Default characters per second while deleting.
pauseDurationnumber1500Default milliseconds to wait after a string finishes typing before deletion begins.
startDelaynumber0Default milliseconds to wait before the first character is typed.
showCursorbooleantrueWhether descendants render a blinking caret by default.
cursorCharacterstring"|"Character used for the blinking caret.
loopbooleantrueWhether descendants cycle their strings indefinitely by default.
classNamestringMerged via cn onto the underlying <span>.
...restReact.ComponentPropsWithoutRef<"span">Forwarded to the underlying <span>.

Data attributes: data-slot="type-ahead-root".

TypeAhead.Static

Inert text segment. Renders a plain <span> — use to weave non-animated copy between Animated segments while keeping the entire tree inline.

PropTypeDefaultDescription
classNamestringMerged via cn onto the underlying <span>.
...restReact.ComponentPropsWithoutRef<"span">Forwarded to the underlying <span>.

Data attributes: data-slot="type-ahead-static".

TypeAhead.Animated

Renders a <span> that types and optionally cycles through one or more strings. All Root settings can be overridden per instance.

PropTypeDefaultDescription
textsstring | string[]requiredA single string to type once, or an array to cycle through.
typingSpeednumberinheritedCharacters per second while typing.
deletionSpeednumberinheritedCharacters per second while deleting.
pauseDurationnumberinheritedMilliseconds to wait after a string is fully typed before deletion begins.
startDelaynumberinheritedMilliseconds to wait before typing the very first character.
showCursorbooleaninheritedWhether to render a blinking caret while the animation runs.
cursorCharacterstringinheritedCharacter used for the blinking caret.
loopbooleantrue for arrays, false otherwiseWhether to keep cycling once the last string is reached. When false, the animation halts on the final string and the caret stops.
render(state: { text, fullText, index, phase, done }) => ReactNodeundefinedRender-prop for fully custom output. Bypasses the default cursor rendering.
onType(text: string, index: number) => voidundefinedFires whenever a string is fully typed.
onDelete(text: string, index: number) => voidundefinedFires whenever a string is fully deleted, just before the next one starts typing.
onComplete() => voidundefinedFires when the animation reaches its terminal state. Only invoked when loop resolves to false.
classNamestringMerged via cn onto the underlying <span>.
...restOmit<React.ComponentPropsWithoutRef<"span">, "children">Forwarded to the underlying <span>.

Data attributes: data-slot="type-ahead-animated", data-phase="idle" | "typing" | "pausing" | "deleting" | "done".

The caret span exposes data-slot="type-ahead-cursor" and is always aria-hidden.

Accessibility

  • Every part renders inline — safe to nest inside <p>, headings, list items or any text container without breaking the HTML content model.
  • The caret is decorative (aria-hidden="true") and never announced.
  • prefers-reduced-motion: reduce snaps TypeAhead.Animated straight to the final string and stops the caret blink, via motion’s useReducedMotion.

Exported types

import type { TypeAheadAnimatedProps, TypeAheadPhase, TypeAheadRootProps, TypeAheadStaticProps, } from "@togetheragency/ui/type-ahead";
Last updated on