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.Rootrenders 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.Staticrenders an inert<span>of text. Use it to weave static copy through an animated sentence without breaking the inline flow.TypeAhead.Animatedruns the typing state machine. Accepts a single string or an array of strings to cycle through. AllRootsettings 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:
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.
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.
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.
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: reducesnapsTypeAhead.Animatedstraight to the final string and stops the caret blink, viamotion’suseReducedMotion.
Exported types
import type {
TypeAheadAnimatedProps,
TypeAheadPhase,
TypeAheadRootProps,
TypeAheadStaticProps,
} from "@togetheragency/ui/type-ahead";