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…

import * as React from "react"
import { cn } from "@/lib/utils"

/**
 * Typing-dot keyframes ship inside the component via a React 19 hoisted
 * <style> — no Tailwind config edits, duplicate bubbles dedupe by href.
 */
const KEYFRAMES = `@keyframes cb-typing{0%,60%,100%{transform:translateY(0);opacity:0.4}30%{transform:translateY(-35%);opacity:1}}`

/** Negative delays start each dot mid-cycle so the bounce reads as a wave, not three synced pulses. */
const DOT_DELAYS = ["[animation-delay:-0.32s]", "[animation-delay:-0.16s]", ""] as const

function TypingDots() {
  return (

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chat-bubble.json

Prompt

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 role prop 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-2xl bubble drops to rounded-*-md, giving the classic speech-bubble pointer with zero pseudo-elements or triangles.
  • Typing indicator supersedes childrentyping is 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-delay values 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.

On This Page