Feedback
Typing Indicator
A chat presence indicator — optional avatar, a receiver-side bubble of waving dots, and a name line that collapses for groups.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/typing-indicator.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "TypingIndicator" component using
class-variance-authority (cva) for variants.
Contract
- Export a forwardRef div extending React.HTMLAttributes<HTMLDivElement> plus
VariantProps of the cva config; remaining props spread on the root.
- Props: variant = "bubble" | "inline" (default "bubble");
size = "sm" | "md" | "lg" (default "md"); name?: string;
names?: string[] (wins over name); avatar?: { src: string; alt: string };
className merged last via cn().
- The component owns no timers and no visibility logic: the consumer mounts it
while a peer is typing and unmounts it when the message arrives.
Behavior
- Sentence building from the people list: [] -> "Someone is typing…";
[a] -> "a is typing…"; [a, b] -> "a and b are typing…";
3+ -> "a and N others are typing…". Build it once per render — never
animate or mutate the text.
- The sentence is visible above the bubble (or after the dots in inline mode)
when at least one name is known; with no names it is rendered sr-only so the
live region still says something meaningful.
- Root is role="status" + aria-live="polite". Because the announced text is a
single static sentence, screen readers speak it once instead of narrating
every animation frame; the dots themselves are aria-hidden.
- Three dots share one keyframe (translateY 0 to -35% with opacity .4 to 1,
~1.1s) with delays 0 / 0.16s / 0.32s — a left-to-right wave. Match the
keyframe shape to your chat bubble's own typing dots so an indicator dropped
at the end of a thread looks like it belongs to the same family.
- Reduced motion stops the animation but keeps the dots readable: each one
rests at a different opacity (0.4 / 0.7 / 1) written as an inline custom
property and applied from a @media (prefers-reduced-motion: reduce) rule in
the component's own stylesheet — a wave frozen mid-cycle, not three bullets.
- Avatar: render an <img> that flips to an initial-letter circle when the image
fails. Two paths lead there and both are required: the onError handler, and a
ref callback that on mount checks `img.complete && img.naturalWidth === 0` —
a server-rendered (or cached) image can finish failing before hydration
attaches onError, and that event never comes back. Store the failed src
rather than a boolean, and derive `failed = failedSrc === src`, so swapping
the avatar to a different person retries the new image instead of inheriting
the previous one's verdict. The initial comes from the first name, falling
back to the alt text. Never leave a broken image in a chat thread.
- Ship the keyframe + the reduced-motion rule in one React 19 hoisted
<style href precedence="medium"> tag — no Tailwind config edits, duplicate
indicators dedupe to a single style tag.
Rendering & styling
- Semantic tokens only. bubble variant mirrors an incoming chat message:
rounded-2xl with rounded-bl-md on the tail corner, bg-muted + text-foreground,
avatar as a bordered circle on bg-muted; the name line is
text-muted-foreground. inline variant drops the bubble and renders dots plus
the sentence in text-muted-foreground for a thread footer or presence row.
- Dots are bg-current, so they take the bubble's foreground token in bubble
mode and the muted token inline — one dot implementation, two surfaces.
- Size scales avatar (size-6/8/10), bubble padding, dot diameter and the text
size together, so all three read as one component at any density.
Customization levers
- Threshold for collapsing names: swap the 2-name branch for "a, b and N
others" if your product shows more presence detail.
- Bubble skin: the rounded-* / bg-muted classes are the only bubble styling —
point them at whatever your message bubble uses (including the sender-side
bg-primary look) and the dots follow via bg-current.
- Motion strength: dot travel (-35%), cycle (1.1s) and delay step (0.16s);
lower the travel for dense inbox lists.
- Structure: drop the avatar for compact threads, or drop the name line and
keep it sr-only when the thread header already says who is talking.
- Sizes: extend the size record — every part class lives in one map.Concepts
- Presence signal — this component claims "a specific someone is composing", which is a social fact, not a technical wait; that is what separates it from a generic loader.
- Announce once — the live region carries a single static sentence, so assistive tech says "Ada is typing…" one time instead of narrating a moving dot trio.
- Name collapsing — group chats degrade gracefully from a name, to two names, to "and N others", so the line never wraps into a roster.
- Receiver-side geometry — reusing the incoming bubble's radius and
bg-mutedmakes the indicator occupy exactly the shape the real message will take, so nothing jumps when it arrives. - Avatar fallback — a flip to an initial circle keeps a broken CDN from leaving a torn image in the middle of a conversation; because a server-rendered image can fail before React attaches
onError, the mount also probescomplete && naturalWidth === 0, and the verdict is stored persrcso the next person's avatar starts clean. - Reduced-motion resting ramp — with animation off, the frozen opacity staircase still reads as a wave; the meaning survives without any movement.