CarsXE

Search docs

Search pages, components, and skills.

Bubble

Conversational messages with optional avatars, copying, timestamps, and actions. Controls appear on hover or keyboard focus and stay visible on touch screens. Timestamps use English and the local timezone; pass a Date, ISO 8601 string, or epoch milliseconds. Invalid dates are omitted.
CX
How can I help with this vehicle?
AM
Show its latest valuation.

Installation

Import from the package. Do not run shadcn add in your app.

tsx
import { Bubble } from "@carsxe/design-system/components/bubble"

Usage

tsx
import * as React from "react"
import { ThumbsUp, RotateCcw } from "lucide-react"
import {
  Bubble,
  BubbleContent,
  BubbleGroup,
  BubbleReactions,
} from "@carsxe/design-system/components/bubble"

export function BubbleConversation({ avatars = true }: { avatars?: boolean }) {
  const [feedback, setFeedback] = React.useState("")
  const assistantMessage = "How can I help with this vehicle?"
  const userMessage = "Show its latest valuation."

  return (
    <div className="w-full max-w-xl space-y-3">
      <BubbleGroup className="gap-5">
        <Bubble
          copyText={assistantMessage}
          timestamp={new Date(2026, 8, 15, 3, 14)}
          avatar={
            avatars
              ? {
                  name: "CarsXE Assistant",
                  src: "/favicon-32.png",
                  fallback: "CX",
                }
              : undefined
          }
          actions={[
            {
              icon: <ThumbsUp className="size-4" />,
              name: "Helpful response",
              tooltipLabel: "Mark as helpful",
              onPress: () => setFeedback("Thanks for your feedback."),
            },
          ]}
        >
          <BubbleContent>{assistantMessage}</BubbleContent>
        </Bubble>
        <Bubble
          align="end"
          variant="secondary"
          copyText={userMessage}
          timestamp={new Date(2026, 8, 15, 3, 15)}
          avatar={avatars ? { name: "Alex Morgan" } : undefined}
          actions={[
            {
              icon: <RotateCcw className="size-4" />,
              name: "Send again",
              onPress: () => setFeedback("Message sent again."),
            },
          ]}
        >
          <BubbleContent>{userMessage}</BubbleContent>
        </Bubble>
      </BubbleGroup>
      <p role="status" className="min-h-5 text-sm text-muted-foreground">
        {feedback}
      </p>
    </div>
  )
}

export function BubbleWithoutAvatars() {
  return <BubbleConversation avatars={false} />
}

export function BubbleLegacy() {
  return (
    <BubbleGroup className="w-full max-w-xl gap-5">
      <Bubble>
        <BubbleContent>How can I help with this vehicle?</BubbleContent>
        <BubbleReactions>👍</BubbleReactions>
      </Bubble>
      <Bubble align="end" variant="secondary">
        <BubbleContent>Show its latest valuation.</BubbleContent>
      </Bubble>
    </BubbleGroup>
  )
}

Without avatars

How can I help with this vehicle?
Show its latest valuation.

Custom actions with and without tooltips

CX
How can I help with this vehicle?
AM
Show its latest valuation.

Legacy composition and reactions

How can I help with this vehicle?
👍
Show its latest valuation.

API Reference

PropTypeDefault
variant"default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive""default"
align"start" | "end""start"
copyTextstring — exact text copied; omit to hide Copy—
timestampDate | string | number — formatted as September 15, 3:14 AM—
actionsBubbleAction[] — rendered in order after Copy—
actions[].iconReact.ReactNode—
actions[].onPressReact.MouseEventHandler<HTMLButtonElement>—
actions[].namestring — accessible button label—
actions[].tooltipLabelstring (optional)—
avatar{ src?: string; name: string; fallback?: React.ReactNode } — defaults to name initials—
onCopyError(error: unknown) => void—