Bubble
Bubble is the surface-level primitive for a conversation. It owns the alignment, the maximum content width, and the position of an optional reaction region. BubbleContent owns the visible surface. Keeping those responsibilities separate lets an application replace the content layout without reimplementing message alignment.
Bubble is a presentational element. It does not own a message record, a collapsed state, a reaction model, or a click action. Compose those behaviors with application state and existing controls such as Button, Link, Collapsible, Tooltip, and Popover.
Import
use gpui::{div, ParentElement as _, Styled as _};
use gpui_component::{
ActiveTheme as _, Colorize as _, Sizable as _,
bubble::{
Bubble, BubbleContent, BubbleGroup, BubbleReactionSide, BubbleReactions,
BubbleVariant,
},
button::{Button, ButtonVariants as _},
message::MessageAlignment,
};Anatomy and basic usage
The shortest form adds children to the typed content slot:
Bubble::new()
.alignment(MessageAlignment::Start)
.child("Can you review this draft?")Use content(...) when the surface needs its own layout or style target:
Bubble::new()
.alignment(MessageAlignment::Start)
.content(
BubbleContent::new().child(
gpui_component::h_flex()
.gap_2()
.child("Can you review this draft?")
.child("📎"),
),
)The root is min_w_0, grows to the available width only for Ghost, and otherwise has a maximum width of 80% of its parent. Long text wraps inside the content slot when its child allows wrapping. An application that needs a different conversation measure can refine the root with w(...), max_w(...), or a child-specific layout.
The default state is:
| Property | Default | Meaning |
|---|---|---|
| Alignment | unset | The parent may supply alignment; a standalone bubble does not force an edge. |
| Variant | Filled | Primary semantic surface. |
| Reactions | none | No reaction region is rendered. |
| Maximum width | 0.8 of the parent | Applies to regular variants. |
| Surface radius | cx.theme().radius_2xl() | Follows the active theme. |
| Content padding | px_3() / py_2() | Applied by BubbleContent for regular variants. |
Alignment
MessageAlignment::Start and MessageAlignment::End are shared with Message:
Bubble::new()
.alignment(MessageAlignment::Start)
.with_variant(BubbleVariant::Secondary)
.child("Incoming message");
Bubble::new()
.alignment(MessageAlignment::End)
.child("Outgoing message")When a bubble is placed in MessageContent::bubble(...), Message propagates its alignment to the content surface. Leave the bubble alignment unset in that case so the message remains the single owner of horizontal placement. Set it explicitly when a bubble is used on its own or when a custom parent intentionally overrides the message row.
Variants
BubbleVariant selects semantic colors and surface treatment. It does not change the content model:
Bubble::new()
.with_variant(BubbleVariant::Filled)
.child("Primary response");
Bubble::new()
.with_variant(BubbleVariant::Secondary)
.child("Neutral incoming response");
Bubble::new()
.with_variant(BubbleVariant::Muted)
.child("Low-emphasis context");
Bubble::new()
.with_variant(BubbleVariant::Tinted)
.child("Subtle selected or emphasized response");
Bubble::new()
.with_variant(BubbleVariant::Outline)
.child("A response that needs a visible boundary");
Bubble::new()
.with_variant(BubbleVariant::Ghost)
.child("A full-width, unframed message surface");
Bubble::new()
.with_variant(BubbleVariant::Destructive)
.child("The operation failed; explain what the user can do next.")Filled is the default. Ghost removes the surface padding, border, and radius, and can occupy the full row. Destructive uses the semantic destructive color with a theme-aware translucent surface; its meaning must also be present in text or another non-color cue. All other variants keep the regular content surface and derive colors from the active theme.
Rich content and long messages
Bubble children are arbitrary GPUI elements. Compose text, code, files, buttons, or custom layouts without a bubble-specific content enum:
use gpui::{div, Styled as _};
use gpui_component::{h_flex, v_flex, Icon, IconName};
Bubble::new()
.content(
BubbleContent::new().child(
h_flex()
.gap_3()
.items_start()
.child(Icon::new(IconName::FileText))
.child(
v_flex()
.min_w_0()
.child("design-notes.pdf")
.child(div().text_sm().child("PDF · 2.4 MB")),
),
),
)For a long response, keep the child min_w_0() and choose wrapping or truncation at the content boundary. Bubble does not truncate arbitrary children. An application layout can expose a Show more affordance by wrapping the content in Collapsible; the bubble itself has no hidden-text state.
// The state and trigger belong to the application. The same Bubble can be
// rendered in the expanded and collapsed states.
Bubble::new()
.with_variant(BubbleVariant::Ghost)
.content(BubbleContent::new().child(long_response_element))Groups
BubbleGroup is a styleable vertical stack. It does not infer sender identity or remove headers; the application decides which consecutive bubbles belong to one sender:
BubbleGroup::new()
.child(
Bubble::new()
.alignment(MessageAlignment::Start)
.with_variant(BubbleVariant::Secondary)
.child("The first paragraph belongs to Alice."),
)
.child(
Bubble::new()
.alignment(MessageAlignment::Start)
.with_variant(BubbleVariant::Secondary)
.child("The second paragraph uses the same group."),
)Use MessageGroup when the repeated unit is a complete message with avatar, header, body, and footer. Use BubbleGroup when only the surface stack is being repeated.
Reactions and interactive content
BubbleReactions positions a region at the top or bottom edge. Put semantic controls inside it. Use the typed action(Button) builder for a button that should read as part of the reaction surface:
Bubble::new()
.alignment(MessageAlignment::Start)
.with_variant(BubbleVariant::Outline)
.child("This response has feedback.")
.reactions(
BubbleReactions::new()
.side(BubbleReactionSide::Bottom)
.alignment(MessageAlignment::End)
.action(
Button::new("bubble-like")
.ghost()
.small()
.label("Like · 2"),
)
.action(
Button::new("bubble-copy")
.ghost()
.small()
.label("Copy"),
),
)The defaults for BubbleReactions are Bottom and End. For a top-attached region aligned to the leading edge:
BubbleReactions::new()
.side(BubbleReactionSide::Top)
.alignment(MessageAlignment::Start)
.action(Button::new("bubble-more").ghost().xsmall().label("More"))action(Button) tells BubbleReactions that the child is a semantic action. When a reaction region contains any typed action, the container removes its decorative content padding and applies the active theme's full/pill radius to each typed button, so the buttons and reaction surface read as one control group. The supplied Button remains customizable: its variant, size, icon, .on_click(...) callback, and .tooltip(...) are preserved. The typed action owns the pill corner radius so the button stays joined to the reaction surface; use the generic path below when a button needs a different radius. Multiple actions can be added with repeated .action(...) calls.
Use .child(...) for emoji, text, a custom element, or an overlay composition that is not a direct Button. This generic path remains backward-compatible and does not opt that child into the compact action treatment. If the same region also contains any .action(...), the whole reaction region still uses the compact surface layout:
BubbleReactions::new()
.child("👍 2")
.action(
Button::new("bubble-reply")
.ghost()
.xsmall()
.label("Reply"),
)Nested interactive wrappers such as Popover remain on the generic .child(...) path because action(...) accepts a direct Button. If the wrapper's trigger should share the reaction geometry, opt into that layout explicitly with p_0() on the reaction region and a theme-derived full radius on the trigger button. The same escape hatch gives an arbitrary button its own radius or surface treatment.
BubbleReactions::new().p_0().child(
gpui_component::popover::Popover::new("bubble-more")
.trigger(
Button::new("bubble-more-trigger")
.ghost()
.xsmall()
.label("More")
.rounded(cx.theme().radius_full()),
)
.child(Button::new("bubble-copy").label("Copy")),
)The reaction container supplies the default spacing, rounded semantic surface, and contrast border. Caller Styled refinements are applied after those defaults, so an application can customize the reaction surface or the Button itself. There is no separate BubbleAction component or reaction data model; the application owns counts, selected state, and submitted actions. Button focus, disabled state, and keyboard activation remain the responsibility of Button. The current Button accessibility label comes from its visible .label(...) value; a tooltip is supplemental. For a URL inside a bubble use Link; for an in-app command use Button. A tooltip or popover can wrap the relevant child using the existing overlay components.
Custom styling and theme tokens
Bubble, BubbleContent, BubbleGroup, and BubbleReactions implement Styled. Refinements are applied after the component defaults, so callers can adjust spacing, width, typography, borders, backgrounds, and shadows at the appropriate part boundary:
Bubble::new()
.w_full()
.content(
BubbleContent::new()
.rounded(cx.theme().radius_lg)
.bg(cx.theme().group_box)
.text_color(cx.theme().group_box_foreground)
.border_1()
.border_color(cx.theme().border)
.px_4()
.py_3()
.child("Application-owned surface treatment"),
)Use semantic theme roles (primary, muted, group_box, border, destructive, and their foreground colors) instead of raw palette values. Radii come from the active theme, so a custom theme can make all conversation surfaces more square or more rounded consistently. The component uses the shared spacing and typography scale; a product-specific scale should be owned by the surrounding design-system layer and passed through its own builders.
Style the group and reaction region independently when the composition needs a different rhythm:
BubbleGroup::new()
.gap_3()
.child(Bubble::new().child("First"))
.child(Bubble::new().child("Second"));
BubbleReactions::new()
.px_2()
.bg(cx.theme().background)
.border_color(cx.theme().ring)
.action(Button::new("bubble-reaction").ghost().xsmall().label("👍"))Accessibility and state guidance
- Use visible text, an icon with a label, or an accessible
Buttonlabel to communicate reactions and actions. Color and a bubble variant are not sufficient status announcements. - Keep keyboard actions inside
Button,Link,Collapsible,Tooltip, orPopover.BubbleandBubbleReactionsare layout elements and do not create focus targets themselves. - Preserve readable contrast when overriding a surface. Pair a custom background with the matching semantic foreground token or an explicitly verified theme role.
- For loading or generated content, render a meaningful text label and use
ShimmerTextorMarkerfor motion. Respect the application's reduced-motion behavior; the shimmer utility renders static text when reduced motion is requested. - A failed or destructive bubble should include the error and the next action, not only a red surface.
When to use another component
Use Message when sender identity, metadata, or a footer belongs to the same row. Use Marker for a compact status or timeline boundary. Use GroupBox or an application-owned surface for a non-conversational document. Use a plain div()/h_flex() when the row has no shared bubble behavior; adding a bubble only to obtain padding makes the hierarchy harder to read.
API reference
Bubble
| Method | Default | Purpose |
|---|---|---|
new() | filled, no alignment, no reactions | Create a bubble. |
alignment(MessageAlignment) | unset | Place the bubble at the leading or trailing edge. |
with_variant(BubbleVariant) | Filled | Select the semantic surface treatment. |
content(BubbleContent) | empty typed content | Replace the visible content surface; direct children move into it. |
reactions(BubbleReactions) | none | Attach a reaction region. |
Bubble also implements ParentElement for the direct .child(...) form and Styled for root layout refinements.
BubbleContent
| Method | Default | Purpose |
|---|---|---|
new() | empty | Create the visible surface slot. |
.child(...) | — | Add arbitrary GPUI elements. |
Styled methods | component defaults | Refine padding, radius, colors, typography, and layout. |
The parent Bubble supplies its variant and alignment to this slot. A standalone BubbleContent therefore has the default Filled treatment.
BubbleGroup
| Method | Default | Purpose |
|---|---|---|
new() | empty vertical stack | Create a group. |
.child(...) | — | Add consecutive bubbles. |
Styled methods | gap_2() | Refine group spacing and layout. |
BubbleReactions
| Method | Default | Purpose |
|---|---|---|
new() | bottom, end aligned | Create a reaction region. |
side(BubbleReactionSide) | Bottom | Attach it above or below the bubble. |
alignment(MessageAlignment) | End | Align children along the bubble edge. |
action(Button) | — | Add a typed action that shares the reaction surface and full/pill radius. |
.child(...) | — | Add emoji, text, or arbitrary GPUI elements. |
Styled methods | themed reaction surface | Refine spacing, colors, and layout. |
Related types
- [
BubbleVariant] —Filled,Secondary,Muted,Tinted,Outline,Ghost, andDestructive. - [
BubbleReactionSide] —ToporBottom. - [
MessageAlignment] —StartorEnd.