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.

Install

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_kit::{div, ParentElement as _, Styled as _};
use gpui_kit::component::{
    ActiveTheme as _, Colorize as _, Sizable as _,
    bubble::{
        Bubble, BubbleContent, BubbleGroup, BubbleReactionSide, BubbleReactions,
        BubbleVariant,
    },
    button::{Button, ButtonVariants as _},
    message::MessageAlignment,
};
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { BubbleVariant, BubbleAlignment, BubbleReactionSide, BubbleReactions, BubbleReactionRow, BubbleWithReactions, BubbleGroup, Bubble } from "bubble.slint";
use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment as Axis, Color, Element, Length, Task, Theme};

#Anatomy and basic usage

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The shortest form is `text(...)`; a caller that needs its own layout uses
// `content(...)` and colours the children, because an arbitrary child does
// not inherit the surface's ink.
let anatomy = column![
    Bubble::new().text("Can you review this draft?"),
    Bubble::new().variant(Variant::Outline).content(
        Content::new().child(
            row![
                line(
                    "Can you review this draft?",
                    theme::FONT_SIZE_SM,
                    |tokens| tokens.foreground,
                ),
                line("PDF · 2.4 MB", theme::FONT_SIZE_XS, |tokens| {
                    tokens.muted_foreground
                }),
            ]
            .spacing(theme::SPACING_SM)
            .align_y(Axis::Center),
        ),
    ),
]
.spacing(theme::SPACING_MD);

The shortest form adds children to the typed content slot:

Bubble::new()
    .alignment(MessageAlignment::Start)
    .child("Can you review this draft?")
SlintNo Slint version of this example yet.

Use content(...) when the surface needs its own layout or style target:

Bubble::new()
    .alignment(MessageAlignment::Start)
    .content(
        BubbleContent::new().child(
            gpui_kit::component::h_flex()
                .gap_2()
                .child("Can you review this draft?")
                .child("📎"),
        ),
    )
SlintNo Slint version of this example yet.

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:

PropertyDefaultMeaning
AlignmentunsetThe parent may supply alignment; a standalone bubble does not force an edge.
VariantFilledPrimary semantic surface.
ReactionsnoneNo reaction region is rendered.
Maximum width0.8 of the parentApplies to regular variants.
Surface radiuscx.theme().radius_2xl()Follows the active theme.
Content paddingpx_3() / py_2()Applied by BubbleContent for regular variants.

#Alignment

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// `start` is the received turn and the default; `end` is the sent one.
let alignment = column![
    Bubble::new()
        .variant(Variant::Secondary)
        .text("Incoming message"),
    Bubble::new()
        .alignment(Alignment::End)
        .text("Outgoing message"),
]
.spacing(theme::SPACING_MD);

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")
VerticalLayout {
    spacing: 12px;
    alignment: start;
    Bubble {
        show-avatar: false;
        variant: BubbleVariant.secondary;
        text: "Incoming message";
    }
    Bubble {
        show-avatar: false;
        alignment: BubbleAlignment.end;
        text: "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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
let variants = column![
    Bubble::new().text("A strong primary bubble."),
    Bubble::new()
        .variant(Variant::Secondary)
        .text("The neutral secondary bubble."),
    Bubble::new()
        .variant(Variant::Muted)
        .text("A lower-emphasis muted bubble."),
    Bubble::new()
        .variant(Variant::Tinted)
        .text("A softly tinted primary bubble."),
    Bubble::new()
        .variant(Variant::Outline)
        .text("A bordered bubble for rich content."),
    Bubble::new()
        .variant(Variant::Destructive)
        .text("A failed action with its reason in text."),
    Bubble::new()
        .variant(Variant::Ghost)
        .text("Ghost content is unframed and can use the full row width."),
]
.spacing(theme::SPACING_LG);

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.")
VerticalLayout {
    spacing: 16px;
    alignment: start;
    Bubble {
        show-avatar: false;
        text: "A strong primary bubble.";
    }
    Bubble {
        show-avatar: false;
        variant: BubbleVariant.secondary;
        text: "The neutral secondary bubble.";
    }
    Bubble {
        show-avatar: false;
        variant: BubbleVariant.muted;
        text: "A lower-emphasis muted bubble.";
    }
    Bubble {
        show-avatar: false;
        variant: BubbleVariant.tinted;
        text: "A softly tinted primary bubble.";
    }
    Bubble {
        show-avatar: false;
        variant: BubbleVariant.outline;
        text: "A bordered bubble for rich content.";
    }
    Bubble {
        show-avatar: false;
        variant: BubbleVariant.destructive;
        text: "A failed action with its reason in text.";
    }
    Bubble {
        show-avatar: false;
        variant: BubbleVariant.ghost;
        text: "Ghost content is unframed and can use the full row width.";
    }
}

Filled is the default. Ghost removes the surface padding, border, and radius, can occupy the full row, and does not clip its children: there is no surface to clip against, so the shadows and overhanging controls of rich content stay visible. 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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A bubble has no hidden-text state: the application owns the collapsed
// flag and the trigger, and renders the same bubble in either state.
let mut details = column![].spacing(theme::SPACING_SM);
if app.details_open {
    details = details.push(line(
        "Dialog and sheet controls already keep their focus rings visible.",
        theme::FONT_SIZE_SM,
        |tokens| tokens.foreground,
    ));
    details = details.push(line(
        "Update the menu focus token separately from its pointer hover state.",
        theme::FONT_SIZE_SM,
        |tokens| tokens.foreground,
    ));
}
details = details.push(
    Button::new(if app.details_open {
        "Show less"
    } else {
        "Show more"
    })
    .variant(ButtonVariant::Ghost)
    .size(ControlSize::Small)
    .on_press(Message::ToggleDetails),
);

let rich = column![
    Bubble::new()
        .variant(Variant::Secondary)
        .text("A longer message wraps naturally within the bubble's available width."),
    Bubble::new()
        .variant(Variant::Muted)
        .text("The accessibility review found two focus states that need more contrast.",)
        .child(details),
    Bubble::new().text("design-notes.pdf\n2.4 MB · PDF"),
]
.spacing(theme::SPACING_MD);

Bubble children are arbitrary GPUI elements. Compose text, code, files, buttons, or custom layouts without a bubble-specific content enum:

use gpui_kit::{div, Styled as _};
use gpui_kit::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")),
                ),
        ),
    )
SlintNo Slint version of this example yet.

A TextView child follows the variant’s text color, including links, code and tables on the primary surface of Filled, so Markdown replies need no per-variant style.

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))
Bubble {
    show-avatar: false;
    variant: BubbleVariant.muted;
    text: "The accessibility review found two focus states that need more contrast.";
    VerticalLayout {
        spacing: 8px;
        alignment: start;
        if root.details-open: VerticalLayout {
            spacing: 8px;
            Text {
                text: "Dialog and sheet controls already keep their focus rings visible.";
                color: Theme.foreground;
                font-size: Theme.font-size-sm;
                wrap: word-wrap;
            }
            Text {
                text: "Update the menu focus token separately from its pointer hover state.";
                color: Theme.foreground;
                font-size: Theme.font-size-sm;
                wrap: word-wrap;
            }
        }
        Button {
            text: root.details-open ? "Show less" : "Show more";
            icon: "chevrons-up-down";
            variant: ButtonVariant.ghost;
            size: ControlSize.small;
            clicked => {
                root.details-open = !root.details-open;
            }
        }
    }
}

#Groups

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// `Group` infers no sender identity; the application says which consecutive
// bubbles belong to one sender and which edge the stack sits on.
let groups = column![
    Group::new()
        .child(
            Bubble::new()
                .variant(Variant::Secondary)
                .text("Can you tell me what changed?"),
        )
        .child(
            Bubble::new()
                .variant(Variant::Secondary)
                .text("The registry route was stale."),
        ),
    Group::new()
        .align(Alignment::End)
        .child(
            Bubble::new()
                .alignment(Alignment::End)
                .text("I removed the stale route."),
        )
        .child(
            Bubble::new()
                .alignment(Alignment::End)
                .text("The updated registry is ready to review."),
        ),
]
.spacing(theme::SPACING_XL);

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."),
    )
VerticalLayout {
    spacing: 20px;
    alignment: start;
    BubbleGroup {
        Bubble {
            show-avatar: false;
            variant: BubbleVariant.secondary;
            text: "Can you tell me what changed?";
        }
        Bubble {
            show-avatar: false;
            variant: BubbleVariant.secondary;
            text: "The registry route was stale.";
        }
    }
    BubbleGroup {
        align: BubbleAlignment.end;
        Bubble {
            show-avatar: false;
            alignment: BubbleAlignment.end;
            text: "I removed the stale route.";
        }
        Bubble {
            show-avatar: false;
            alignment: BubbleAlignment.end;
            text: "The updated registry is ready to review.";
        }
    }
}

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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A reaction is a pill of plain elements below or above the surface; the
// action inside it is an ordinary `Button` the application owns. A link is
// content, not a command, so it is drawn in the primary ink.
let reactions = column![
    Bubble::new()
        .variant(Variant::Outline)
        .text("This bubble has reaction feedback.")
        .reactions(reaction_pill(Alignment::Start, "Like · 2", "Like")),
    Bubble::new()
        .alignment(Alignment::End)
        .text("Reactions can attach to any edge.")
        .reactions(reaction_pill(Alignment::End, "Thanks · 1", "Thanks")),
    Bubble::new()
        .variant(Variant::Outline)
        .text("The implementation guide is available online.")
        .child(line(
            "Open the component documentation",
            theme::FONT_SIZE_SM,
            |tokens| tokens.primary,
        )),
    Bubble::new().variant(Variant::Muted).content(
        Content::new().child(
            row![
                line(
                    "The generated report is ready.",
                    theme::FONT_SIZE_SM,
                    |tokens| tokens.foreground,
                ),
                Button::new(if app.report_open {
                    "Close report"
                } else {
                    "Open report"
                })
                .variant(ButtonVariant::Ghost)
                .size(ControlSize::Small)
                .on_press(Message::OpenReport),
            ]
            .spacing(theme::SPACING_MD)
            .align_y(Axis::Center),
        )
    ),
    Bubble::new()
        .variant(Variant::Destructive)
        .text("The build command could not finish.")
        .reactions(
            Reactions::new()
                .side(ReactionSide::Top)
                .alignment(Alignment::Start)
                .child(line("Build failed", theme::FONT_SIZE_XS, |tokens| {
                    tokens.foreground
                })),
        ),
]
.spacing(theme::SPACING_XL);

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"),
            ),
    )
SlintNo Slint version of this example yet.

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"))
SlintNo Slint version of this example yet.

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"),
    )
SlintNo Slint version of this example yet.

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_kit::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")),
)
VerticalLayout {
    spacing: 24px;
    padding-top: 8px;
    padding-bottom: 8px;
    alignment: start;
    BubbleWithReactions {
        column-align: BubbleAlignment.start;
        Bubble {
            horizontal-stretch: 0;
            show-avatar: false;
            variant: BubbleVariant.outline;
            text: "This bubble has reaction feedback.";
        }
        BubbleReactionRow {
            align: BubbleAlignment.start;
            BubbleReactions {
                HorizontalLayout {
                    spacing: 4px;
                    alignment: center;
                    Text {
                        text: "👍";
                        font-family: "Noto Emoji";
                        font-size: Theme.font-size-sm;
                        horizontal-stretch: 0;
                    }
                    Text {
                        text: "2";
                        color: Theme.foreground;
                        font-size: Theme.font-size-sm;
                        horizontal-stretch: 0;
                    }
                }
            }
        }
    }
    HorizontalLayout {
        horizontal-stretch: 1;
        alignment: end;
        BubbleWithReactions {
            column-align: BubbleAlignment.end;
            BubbleReactionRow {
                align: BubbleAlignment.end;
                BubbleReactions {
                    HorizontalLayout {
                        spacing: 4px;
                        alignment: center;
                        Text {
                            text: "✨";
                            font-family: "Noto Emoji";
                            font-size: Theme.font-size-sm;
                            horizontal-stretch: 0;
                        }
                        Text {
                            text: "1";
                            color: Theme.foreground;
                            font-size: Theme.font-size-sm;
                            horizontal-stretch: 0;
                        }
                    }
                }
            }
            Bubble {
                horizontal-stretch: 0;
                show-avatar: false;
                alignment: BubbleAlignment.end;
                text: "Reactions can attach to any edge.";
            }
        }
    }
}

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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// Success is the semantic green; the fill is a tint and the label and border
// use the same role, so the contrast stays paired. Every token is resolved
// from the active theme when the surface draws.
let custom = Bubble::new().text("Custom semantic color").content(
    Content::new()
        .fill(|tokens| theme::alpha(tokens.success, 0.15))
        .text_color(|tokens| tokens.success)
        .border_color(|tokens| theme::alpha(tokens.success, 0.35))
        .border_width(1.0)
        .radius(theme::RADIUS_LG)
        .padding_x(16.0)
        .padding_y(12.0),
);

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"),
    )
SlintNo Slint version of this example yet.

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("👍"))
Bubble {
    show-avatar: false;
    // Success is the semantic green. The fill is a tint, the label and
    // border use the same role so the contrast stays paired.
    fill: Theme.success.with-alpha(0.15);
    text-color: Theme.success;
    border-color: Theme.success.with-alpha(0.35);
    border-width: 1px;
    radius: Theme.radius-lg;
    padding-x: 16px;
    padding-y: 12px;
    text: "Custom semantic color";
}

#Accessibility and state guidance

  • Use visible text, an icon with a label, or an accessible Button label to communicate reactions and actions. Color and a bubble variant are not sufficient status announcements.
  • Keep keyboard actions inside Button, Link, Collapsible, Tooltip, or Popover. Bubble and BubbleReactions are 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 ShimmerText or Marker for 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

MethodDefaultPurpose
new()filled, no alignment, no reactionsCreate a bubble.
alignment(MessageAlignment)unsetPlace the bubble at the leading or trailing edge.
with_variant(BubbleVariant)FilledSelect the semantic surface treatment.
content(BubbleContent)empty typed contentReplace the visible content surface; direct children move into it.
reactions(BubbleReactions)noneAttach a reaction region.

Bubble also implements ParentElement for the direct .child(...) form and Styled for root layout refinements.

#BubbleContent

MethodDefaultPurpose
new()emptyCreate the visible surface slot.
.child(...)—Add arbitrary GPUI elements.
Styled methodscomponent defaultsRefine 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

MethodDefaultPurpose
new()empty vertical stackCreate a group.
.child(...)—Add consecutive bubbles.
Styled methodsgap_2()Refine group spacing and layout.

#BubbleReactions

MethodDefaultPurpose
new()bottom, end alignedCreate a reaction region.
side(BubbleReactionSide)BottomAttach it above or below the bubble.
alignment(MessageAlignment)EndAlign 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 methodsthemed reaction surfaceRefine spacing, colors, and layout.
  • [BubbleVariant] — Filled, Secondary, Muted, Tinted, Outline, Ghost, and Destructive.
  • [BubbleReactionSide] — Top or Bottom.
  • [MessageAlignment] — Start or End.

#BubbleVariant

filled, secondary, muted, tinted, outline, ghost, destructive

#BubbleAlignment

start, end

#BubbleReactionSide

top, bottom

#BubbleReactions

Pill for emoji, labels, or icon buttons. Size to the row; do not stretch in a column.

#BubbleReactionRow

Aligns a content-sized reaction pill under a bubble edge (GPUI `left-3` / `right-3`).

NameKindTypeDefault
aligninBubbleAlignmentBubbleAlignment.start

#BubbleWithReactions

Bubble plus a reaction pill with stable spacing (no negative overlap clipping).

NameKindTypeDefault
column-aligninBubbleAlignmentBubbleAlignment.start

#BubbleGroup

Consecutive bubbles from one sender.

NameKindTypeDefault
aligninBubbleAlignmentBubbleAlignment.start

#Bubble

A chat bubble. Mirrors GPUI Component's `Bubble`: variants, alignment, optional avatars, timestamps, grouped stacks, and composable content.

NameKindTypeDefaultDescription
roleinMessageRoleMessageRole.assistant
alignmentinBubbleAlignmentrole == MessageRole.user ? BubbleAlignment.end : BubbleAlignment.start
variantinBubbleVariantBubbleVariant.filled
textinstring
timeinstring
show-avatarinbooltrue
fillincolorvariant-fillEach surface property defaults to the variant. Setting one replaces that default, the same way a Styled refinement does in GPUI.
text-colorincolorvariant-text
border-colorincolorvariant-border
border-widthinlengthvariant == BubbleVariant.outline ? 1px : 0px
radiusinlengthghost ? 0px : Theme.radius-xl
padding-xinlengthghost ? 0px : 12px
padding-yinlengthghost ? 0px : 8px