ZaidanHomeDocsComponentsBlocksChartsTypesetCreate

Command Palette

Search for a command to run...

GitHub
New
Sections
  • Introduction
  • Components
  • Blocks
  • Installation
  • Customization
  • Dark Mode
  • Typeset
  • Zaidan Skills
  • FAQ
  • Roadmap
  • Changelog
Installation
  • Astro
  • Manual
  • Solid Start
  • TanStack Router
  • TanStack Start
  • Vite
Blocks
  • Data Grid
  • Event Calendar
  • Filters
  • Gantt
  • Image Crop
  • Kanban
  • Message Scroller
  • Questionnaire
  • Sortable
Components
  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Marker
  • Menubar
  • Message
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip

Bubble

Displays conversational content in a message bubble. Supports variants, alignment, grouping, reactions, and collapsible content.

Hey there! What's up?
Hey! Want to see chat bubbles?
I can group messages, switch sides, and keep the whole thread easy to scan.
👍
Sure. Hit me with your best demo.
Yes. You are reading a demo that is demoing itself. Very meta. Very on-brand.
👍🔥👀+2
1
import { Bubble, BubbleContent, BubbleGroup, BubbleReactions } from "~/components/ui/bubble";
2
3
export default function BubbleDemo() {
4
return (
5
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
6
<Bubble align="end">
7
<BubbleContent>Hey there! What&apos;s up?</BubbleContent>
8
</Bubble>
9
<BubbleGroup>
10
<Bubble variant="muted">
11
<BubbleContent>Hey! Want to see chat bubbles?</BubbleContent>
12
</Bubble>
13
<Bubble variant="muted">
14
<BubbleContent>
15
I can group messages, switch sides, and keep the whole thread easy to scan.
16
</BubbleContent>
17
<BubbleReactions role="img" aria-label="Reaction: thumbs up">
18
<span>👍</span>
19
</BubbleReactions>
20
</Bubble>
21
</BubbleGroup>
22
<Bubble align="end">
23
<BubbleContent>Sure. Hit me with your best demo.</BubbleContent>
24
</Bubble>
25
<Bubble variant="muted">
26
<BubbleContent>
27
Yes. You are reading a demo that is demoing itself. Very meta. Very on-brand.
28
</BubbleContent>
29
<BubbleReactions role="img" aria-label="Reactions: thumbs up, fire, eyes, and 2 more">
30
<span>👍</span>
31
<span>🔥</span>
32
<span>👀</span>
33
<span>+2</span>
34
</BubbleReactions>
35
</Bubble>
36
</div>
37
);
38
}

The Bubble component displays framed conversational content. Use it for chat text, short structured output, quoted replies, suggestions, and reactions.

For full-featured chat interfaces, use the Message component. Bubble is intentionally scoped to the bubble surface. Place avatars, names, timestamps, metadata, and message-level actions in Message.

Installation

pnpm dlx shadcn@latest add @zaidan/bubble
npx shadcn@latest add @zaidan/bubble
yarn dlx shadcn@latest add @zaidan/bubble
bunx --bun shadcn@latest add @zaidan/bubble

Usage

1
import { Bubble, BubbleContent, BubbleReactions } from "~/components/ui/bubble";
1
<Bubble>
2
<BubbleContent>
3
I checked the registry output and removed the stale route.
4
</BubbleContent>
5
<BubbleReactions role="img" aria-label="Reaction: thumbs up">
6
<span>👍</span>
7
</BubbleReactions>
8
</Bubble>

Composition

Use the following composition to build a bubble:

Bubble
├── BubbleContent
└── BubbleReactions

Use BubbleGroup to group consecutive bubbles from the same sender:

BubbleGroup
├── Bubble
│ └── BubbleContent
└── Bubble
└── BubbleContent

Features

  • Seven visual variants, from a strong primary bubble to unframed ghost content.
  • Start and end alignment for sender and receiver bubbles.
  • Reactions that anchor to the bubble edge with configurable side and alignment.
  • Bubbles size to their content, up to 80% of the container width.
  • Polymorphic content via as for link and button bubbles.
  • Customizable styling through the class prop on every part.

Variants

Use variant to change the visual treatment of the bubble.

This is the default primary bubble.
This is the secondary variant.
This one is muted. It uses a lower emphasis color for the chat bubble.
👍
This one is tinted. The tint is a softer color derived from the primary color.
We can also use an outlined variant.
Or a destructive variant with a reaction.
🔥

Ghost bubbles work for rich text, inline code, and other content that should not be framed.

This is perfect for assistant messages that can take the full width of the container.

1
import { Bubble, BubbleContent, BubbleReactions } from "~/components/ui/bubble";
2
3
export default function BubbleVariants() {
4
return (
5
<div class="flex w-full max-w-sm flex-col gap-12 py-12">
6
<Bubble>
7
<BubbleContent>This is the default primary bubble.</BubbleContent>
8
</Bubble>
9
<Bubble variant="secondary" align="end">
10
<BubbleContent>This is the secondary variant.</BubbleContent>
11
</Bubble>
12
<Bubble variant="muted">
13
<BubbleContent>
14
This one is muted. It uses a lower emphasis color for the chat bubble.
15
</BubbleContent>
16
<BubbleReactions role="img" aria-label="Reaction: thumbs up">
17
<span>👍</span>
18
</BubbleReactions>
19
</Bubble>
20
<Bubble variant="tinted" align="end">
21
<BubbleContent>
22
This one is tinted. The tint is a softer color derived from the primary color.
23
</BubbleContent>
24
</Bubble>
25
<Bubble variant="outline">
26
<BubbleContent>We can also use an outlined variant.</BubbleContent>
27
</Bubble>
28
<Bubble variant="destructive" align="end">
29
<BubbleContent>Or a destructive variant with a reaction.</BubbleContent>
30
<BubbleReactions role="img" aria-label="Reaction: fire">
31
<span>🔥</span>
32
</BubbleReactions>
33
</Bubble>
34
<Bubble variant="ghost">
35
<BubbleContent>
36
<p>
37
Ghost bubbles work for <strong>rich text</strong>, inline <code>code</code>, and other
38
content that should not be framed.
39
</p>
40
<p class="mt-4">
41
This is perfect for assistant messages that can take the full width of the container.
42
</p>
43
</BubbleContent>
44
</Bubble>
45
</div>
46
);
47
}
VariantDescription
defaultA strong primary bubble, usually for the current user.
secondaryThe standard neutral bubble for conversation content.
mutedA lower-emphasis bubble for quiet supporting content.
tintedA subtle primary-tinted bubble.
outlineA bordered bubble for secondary or rich content.
ghostUnframed content for assistant text or rich content.
destructiveA destructive bubble for error or failed actions.

A bubble sizes to its content, up to 80% of the container width. The ghost variant removes the max-width so assistant text and rich content can span the full row.

Alignment

Use align on Bubble to align the bubble to the start or end of the conversation.

This bubble is aligned to the start. This is the default alignment.
This bubble is aligned to the end. Use this for user messages.
1
import { Bubble, BubbleContent } from "~/components/ui/bubble";
2
3
export default function BubbleAlignment() {
4
return (
5
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
6
<Bubble variant="muted">
7
<BubbleContent>
8
This bubble is aligned to the start. This is the default alignment.
9
</BubbleContent>
10
</Bubble>
11
<Bubble align="end">
12
<BubbleContent>
13
This bubble is aligned to the end. Use this for user messages.
14
</BubbleContent>
15
</Bubble>
16
</div>
17
);
18
}
alignDescription
startAlign the bubble to the start of the conversation.
endAlign the bubble to the end of the conversation.

When building chat interfaces, you will often apply alignment to a surrounding message component rather than directly to Bubble.

Bubble Group

Use BubbleGroup to group consecutive bubbles from the same sender. Set align on each Bubble, not on BubbleGroup.

Can you tell me what's the issue?
You tell me!
It worked yesterday. You broke it!
Find the bug and fix it.
👀
Want me to diff yesterday's you against today's you? It's a bit embarrassing.
1
import { Bubble, BubbleContent, BubbleGroup, BubbleReactions } from "~/components/ui/bubble";
2
3
export default function BubbleGroupDemo() {
4
return (
5
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
6
<Bubble variant="muted">
7
<BubbleContent>Can you tell me what&apos;s the issue?</BubbleContent>
8
</Bubble>
9
<BubbleGroup>
10
<Bubble align="end">
11
<BubbleContent>You tell me!</BubbleContent>
12
</Bubble>
13
<Bubble align="end">
14
<BubbleContent>It worked yesterday. You broke it!</BubbleContent>
15
</Bubble>
16
<Bubble align="end">
17
<BubbleContent>Find the bug and fix it.</BubbleContent>
18
<BubbleReactions role="img" aria-label="Reactions: eyes" align="start">
19
<span>👀</span>
20
</BubbleReactions>
21
</Bubble>
22
</BubbleGroup>
23
<Bubble variant="muted">
24
<BubbleContent>
25
Want me to diff yesterday&apos;s you against today&apos;s you? It&apos;s a bit
26
embarrassing.
27
</BubbleContent>
28
</Bubble>
29
</div>
30
);
31
}

Links and Buttons

Turn a bubble into a link or button with the as prop on BubbleContent.

How can I help you today?
1
import { toast } from "solid-sonner";
2
import { Bubble, BubbleContent, BubbleGroup } from "~/components/ui/bubble";
3
import { Toaster } from "~/components/ui/toast";
4
5
export default function BubbleLinkButton() {
6
const notify = (description: string) => toast.success(description);
7
8
return (
9
<>
10
<Toaster />
11
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
12
<Bubble variant="muted">
13
<BubbleContent>How can I help you today?</BubbleContent>
14
</Bubble>
15
<BubbleGroup>
16
<Bubble variant="tinted" align="end">
17
<BubbleContent
18
as="button"
19
type="button"
20
onClick={() => notify("You clicked forgot password")}
21
>
22
I forgot my password
23
</BubbleContent>
24
</Bubble>
25
<Bubble variant="tinted" align="end">
26
<BubbleContent
27
as="button"
28
type="button"
29
onClick={() => notify("You clicked help with subscription")}
30
>
31
I need help with my subscription
32
</BubbleContent>
33
</Bubble>
34
<Bubble variant="tinted" align="end">
35
<BubbleContent
36
as="button"
37
type="button"
38
onClick={() => notify("You clicked something else. Talk to a human.")}
39
>
40
Something else. Talk to a human.
41
</BubbleContent>
42
</Bubble>
43
</BubbleGroup>
44
</div>
45
</>
46
);
47
}
1
<Bubble variant="muted">
2
<BubbleContent as="button" type="button" onClick={onReply}>
3
Click here
4
</BubbleContent>
5
</Bubble>

Reactions

Use BubbleReactions for bubble reactions or quick action buttons. Use side and align to position the row. Reactions overlap the bubble edge, so leave vertical space between rows.

I don't need tests, I know my code works.
👍😮
Bold. Fine I'll add some tests. I'll let you know when they're done.
👀🚀+2
Tests passed on the first try. All 142 of them. Looking good!
🎉👏
Are you sure I can run this command?
1
import { toast } from "solid-sonner";
2
import { Bubble, BubbleContent, BubbleReactions } from "~/components/ui/bubble";
3
import { Button } from "~/components/ui/button";
4
import { Toaster } from "~/components/ui/toast";
5
6
export default function BubbleReactionsDemo() {
7
return (
8
<>
9
<Toaster />
10
<div class="flex w-full max-w-sm flex-col gap-12 py-12">
11
<Bubble variant="muted" align="end">
12
<BubbleContent>I don&apos;t need tests, I know my code works.</BubbleContent>
13
<BubbleReactions align="start" role="img" aria-label="Reactions: thumbs up, surprised">
14
<span>👍</span>
15
<span>😮</span>
16
</BubbleReactions>
17
</Bubble>
18
<Bubble variant="muted">
19
<BubbleContent>
20
Bold. Fine I&apos;ll add some tests. I&apos;ll let you know when they&apos;re done.
21
</BubbleContent>
22
<BubbleReactions role="img" aria-label="Reactions: eyes, rocket, and 2 more">
23
<span>👀</span>
24
<span>🚀</span>
25
<span>+2</span>
26
</BubbleReactions>
27
</Bubble>
28
<Bubble align="end">
29
<BubbleContent>
30
Tests passed on the first try. All 142 of them. Looking good!
31
</BubbleContent>
32
<BubbleReactions
33
side="top"
34
align="start"
35
role="img"
36
aria-label="Reactions: party popper, clapping hands"
37
>
38
<span>🎉</span>
39
<span>👏</span>
40
</BubbleReactions>
41
</Bubble>
42
<Bubble variant="destructive">
43
<BubbleContent>Are you sure I can run this command?</BubbleContent>
44
<BubbleReactions>
45
<Button
46
variant="ghost"
47
size="xs"
48
onClick={() => toast.success("You clicked yes, running command...")}
49
>
50
Yes, run it
51
</Button>
52
</BubbleReactions>
53
</Bubble>
54
</div>
55
</>
56
);
57
}

Show More / Collapsible

Long bubble content can be composed with Collapsible to provide a show more or show less interaction.

How can I help you today?

The accessibility review found two focus states that were visually too subtle in dark mode. I checked the dialog, menu, and drawer paths because each one renders focusable control...

1
import { ChevronDownIcon } from "lucide-solid";
2
import { createSignal, Show } from "solid-js";
3
import { Bubble, BubbleContent } from "~/components/ui/bubble";
4
import { Button } from "~/components/ui/button";
5
import {
6
Collapsible,
7
CollapsibleContent,
8
CollapsibleTrigger,
9
} from "~/components/ui/collapsible";
10
11
const text = `The accessibility review found two focus states that were visually too subtle in dark mode.
12
13
I checked the dialog, menu, and drawer paths because each one renders focusable controls inside a layered surface.
14
15
The dialog and drawer are fine. The menu needs the hover and focus tokens split so keyboard focus stays visible when the pointer is not involved.
16
17
I also recommend keeping the change in the style file instead of the primitive so the other themes can choose their own focus treatment later.`;
18
19
const previewLength = 180;
20
21
export default function BubbleCollapsible() {
22
const [open, setOpen] = createSignal(false);
23
const preview = `${text.slice(0, previewLength)}...`;
24
25
return (
26
<div class="flex w-full max-w-sm flex-col gap-8 py-12">
27
<Bubble variant="muted">
28
<BubbleContent>How can I help you today?</BubbleContent>
29
</Bubble>
30
<Bubble variant="muted" align="end">
31
<BubbleContent class="whitespace-pre-line">
32
<Collapsible open={open()} onOpenChange={setOpen}>
33
<Show when={!open()}>
34
<p>{preview}</p>
35
</Show>
36
<CollapsibleContent>
37
<p>{text}</p>
38
</CollapsibleContent>
39
<CollapsibleTrigger
40
as={Button}
41
variant="link"
42
class="group mt-2 h-auto gap-1 p-0 text-muted-foreground"
43
>
44
{open() ? "Show less" : "Show more"}
45
<ChevronDownIcon class="transition-transform group-data-[expanded]:rotate-180" />
46
</CollapsibleTrigger>
47
</Collapsible>
48
</BubbleContent>
49
</Bubble>
50
</div>
51
);
52
}

Tooltip

Wrap a bubble action in a Tooltip to reveal metadata on hover or keyboard focus, such as when a message was read.

Did you remove the stale route?
Yes, removed it from the registry.
1
import { CheckIcon } from "lucide-solid";
2
import { Bubble, BubbleContent, BubbleReactions } from "~/components/ui/bubble";
3
import { Button } from "~/components/ui/button";
4
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip";
5
6
export default function BubbleTooltip() {
7
return (
8
<div class="flex w-full max-w-sm flex-col gap-4 py-12">
9
<Bubble variant="secondary">
10
<BubbleContent>Did you remove the stale route?</BubbleContent>
11
</Bubble>
12
<Bubble align="end">
13
<BubbleContent>Yes, removed it from the registry.</BubbleContent>
14
<BubbleReactions>
15
<Tooltip>
16
<TooltipTrigger
17
as={Button}
18
variant="ghost"
19
size="icon-xs"
20
aria-label="Message read details"
21
>
22
<CheckIcon />
23
</TooltipTrigger>
24
<TooltipContent>Read on Jan 5, 2026 at 4:32 PM</TooltipContent>
25
</Tooltip>
26
</BubbleReactions>
27
</Bubble>
28
</div>
29
);
30
}

Popover

Pair a bubble action with a Popover to surface more information on demand, such as the full error message for a failed action.

Run the build script.
Failed to run the command.
1
import { InfoIcon } from "lucide-solid";
2
import { Bubble, BubbleContent, BubbleReactions } from "~/components/ui/bubble";
3
import { Button } from "~/components/ui/button";
4
import {
5
Popover,
6
PopoverContent,
7
PopoverDescription,
8
PopoverHeader,
9
PopoverTitle,
10
PopoverTrigger,
11
} from "~/components/ui/popover";
12
13
export default function BubblePopover() {
14
return (
15
<div class="flex w-full max-w-sm flex-col gap-4 py-12">
16
<Bubble align="end">
17
<BubbleContent>Run the build script.</BubbleContent>
18
</Bubble>
19
<Bubble variant="destructive">
20
<BubbleContent>Failed to run the command.</BubbleContent>
21
<BubbleReactions>
22
<Popover>
23
<PopoverTrigger
24
as={Button}
25
variant="ghost"
26
size="icon-xs"
27
aria-label="Show error details"
28
class="aria-expanded:text-destructive"
29
>
30
<InfoIcon />
31
</PopoverTrigger>
32
<PopoverContent>
33
<PopoverHeader>
34
<PopoverTitle class="text-sm">Command failed with exit code 1</PopoverTitle>
35
<PopoverDescription class="text-sm">
36
ENOENT: no such file or directory, open pnpm-lock.yaml
37
</PopoverDescription>
38
</PopoverHeader>
39
</PopoverContent>
40
</Popover>
41
</BubbleReactions>
42
</Bubble>
43
</div>
44
);
45
}

Accessibility

Bubble renders the presentational message surface. Keep conversation-level semantics on the surrounding container and follow the guidance below.

Labeling reactions

Group decorative emoji reactions as a single image with a descriptive aria-label so assistive technology announces the reaction set once.

1
<BubbleReactions role="img" aria-label="Reactions: thumbs up, fire, and 8 more">
2
<span>👍</span>
3
<span>🔥</span>
4
<span>+8</span>
5
</BubbleReactions>

When reactions are interactive, render buttons and give icon-only buttons an aria-label.

1
<BubbleReactions>
2
<Button aria-label="Thumbs up" variant="secondary" size="icon-xs">
3
<ThumbsUpIcon />
4
</Button>
5
</BubbleReactions>

Interactive bubbles

When a bubble is clickable, render it as a real <button> or <a> with as so it is focusable and exposes the correct role. BubbleContent supplies a visible focus ring for interactive elements, and the bubble text provides its accessible name.

1
<Bubble variant="muted" align="end">
2
<BubbleContent as="button" type="button" onClick={onReply}>
3
I forgot my password
4
</BubbleContent>
5
</Bubble>

Meaning beyond color

Bubble variants signal role and tone with color. Pair them with text, alignment, or icons so meaning is not conveyed by color alone. For a destructive bubble, keep the error context in the message text rather than relying on the color treatment.

API Reference

Bubble

The root bubble wrapper.

PropTypeDefaultDescription
variant"default" | "secondary" | "muted" | "tinted" | "outline" | "ghost" | "destructive""default"The bubble visual treatment.
align"start" | "end""start"The inline alignment of the bubble.
classstring-Additional classes to apply to the root element.

BubbleContent

The bubble content wrapper.

PropTypeDefaultDescription
asValidComponent"div"Render the content as a different element, such as a link or button.
classstring-Additional classes to apply to the content element.

BubbleReactions

Displays overlapped reactions for a bubble.

PropTypeDefaultDescription
side"top" | "bottom""bottom"The side of the bubble to anchor the reactions.
align"start" | "end""end"The inline alignment of the reactions.
classstring-Additional classes to apply to the reaction row.

BubbleGroup

Groups consecutive bubbles from the same sender.

PropTypeDefaultDescription
classstring-Additional classes to apply to the group root.

On This Page

  • Installation
  • Usage
  • Composition
  • Features
  • Variants
  • Alignment
  • Bubble Group
  • Links and Buttons
  • Reactions
  • Show More / Collapsible
  • Tooltip
  • Popover
  • Accessibility
    • Labeling reactions
    • Interactive bubbles
    • Meaning beyond color
  • API Reference
    • Bubble
    • BubbleContent
    • BubbleReactions
    • BubbleGroup
Built by Kevin Abatan. The source code is available on GitHub.