Display
Chat Bubble
A single message bubble — role-based alignment and color, avatar slot, and a bouncing typing indicator — for assembling chat and support message lists.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chat-bubble.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "ChatBubble" component (no runtime deps
beyond the cn() class-merge helper).
Contract
- Export a forwardRef ChatBubble extending
Omit<React.HTMLAttributes<HTMLDivElement>, "role">, ref on the root div.
- Props: role: "user" | "assistant" (required); avatar?: ReactNode; name?:
string; timestamp?: string (pre-formatted — the component never parses or
formats dates itself); typing?: boolean (default false); children: ReactNode;
className merged via cn().
Behavior
- role drives everything together: "user" right-aligns the row (flex-row-reverse),
uses bg-primary/text-primary-foreground, and drops the bubble's bottom-right
corner to rounded-br-md (the speech-bubble "tail"). "assistant" left-aligns,
uses bg-muted/text-foreground, and drops the bottom-left corner instead
(rounded-bl-md). No triangle pseudo-element — just one corner of a
rounded-2xl box.
- The row is `flex items-end gap-2`: avatar (if passed) sits beside a column
that stacks name (small muted label) → bubble → timestamp (small muted
label), bottom-aligned as a group.
- typing=true ignores children completely and renders a three-dot bouncing
indicator inside the bubble instead — it's a rendering mode, not an overlay,
so callers never need to conditionally omit children themselves. The
indicator carries aria-label="Typing…" and role="status". Each dot uses a
negative animation-delay (0, -0.16s, -0.32s equivalent stagger) so the
bounce reads as a left-to-right wave, not three dots pulsing in lockstep.
Keyframes ship inside the component via a React 19 hoisted
<style href="..." precedence="medium"> tag so no Tailwind config edits are
needed and duplicate bubbles dedupe to one style tag.
- Under prefers-reduced-motion, the dot animation is set to `none` — the three
dots stay visible and solid, just static. The "someone is typing" signal
never disappears, only the motion does.
- No internal state, no timers, no browser APIs — a pure render function.
Rendering & styling
- Semantic tokens only: bg-primary / text-primary-foreground for the user
bubble, bg-muted / text-foreground for the assistant bubble,
text-muted-foreground for the name and timestamp labels, bg-current for the
typing dots (so they inherit whichever bubble's foreground color they sit
in). No hardcoded colors anywhere.
- Bubble: rounded-2xl px-4 py-2.5 text-sm leading-relaxed break-words. The 75%
width cap (max-w-[75%]) lives on the column wrapper (name + bubble +
timestamp), not the bubble alone, so all three stay capped together and
never drift wider than the bubble under them.
- cn() merges the consumer's className onto the root row; every other native
div prop is spread onto the root via forwardRef.
- No "use client" directive: there are no hooks or browser APIs — the typing
animation is pure CSS — so this renders as a Server Component wherever it's
dropped in.
Customization levers
- Bubble width cap: the 75% max-width lives on the column wrapper — tighten
to max-w-[60%] for a narrower thread, or loosen it for a wide sidebar panel.
- Color inversion: swap which role gets bg-primary vs bg-muted (and their
foreground pairs) to flip which side reads as "emphasized" — e.g. give the
assistant bg-primary for a more branded-bot feel.
- Message-group spacing: this component renders exactly one bubble; the
vertical gap between consecutive bubbles from the same sender is a
consumer-side lever in the list that renders them (tighten to gap-1 for
same-sender runs, open up to gap-4 across a sender change) —
ChatBubble stays stateless about grouping.
- Markdown rendering: children accepts any ReactNode, so swap plain text for
the output of a markdown renderer (e.g. react-markdown) with zero changes
to the component itself.
- Avatar treatment: the avatar slot accepts any node — a plain <img>, a
shadcn Avatar with an initials fallback, or an icon glyph for system
messages.Concepts
- Role-based alignment — a single
roleprop flips side, color, and tail corner together, so a message list only ever passes data, never layout classes. - Tail corner — one corner of the
rounded-2xlbubble drops torounded-*-md, giving the classic speech-bubble pointer with zero pseudo-elements or triangles. - Typing indicator supersedes children —
typingis a rendering mode, not a decoration layered on top: children are ignored entirely so callers never conditionally strip them out. - Phase-staggered dots — negative
animation-delayvalues start each dot mid-cycle, producing a left-to-right wave instead of three dots pulsing in lockstep. - Reduced-motion degrades to static, not hidden — turning off the animation still renders three solid dots, so the "someone is typing" signal survives even with motion disabled.
- Consumer-formatted timestamp — the component takes a ready-to-display string, never a
Date; locale and relative-time formatting stay the caller's concern.
Empty State
A centered empty-state panel — icon chip, title, description and up to two actions — for any non-data-contract blank surface.
Relative Time
Auto-refreshing "time ago" text — renders a real `<time>` element, formats with Intl.RelativeTimeFormat, and stays hydration-safe by reading the clock through useSyncExternalStore.