Skip to content
Glint UI

MessageBubble

One turn of a chat transcript — a role-tinted surface carrying the turn's text, its slotted content and its reactions.

Usage

import { MessageBubble, MessageRole } from "@glint/components/message-bubble.slint";
export component AppWindow inherits Window {
VerticalLayout {
spacing: 12px;
MessageBubble {
role: MessageRole.user;
text: "Refactor this.";
}
// `text` is the common case — one wrapped paragraph. Anything else is
// slotted, and stacks under it.
reply := MessageBubble {
role: MessageRole.agent;
text: "Done — here's the diff.";
Rectangle {
height: 80px;
border-radius: 6px;
background: reply.surface-color;
}
}
}
}

A bubble is a full-width transcript row. The row spans the transcript and the surface inside it does not: role decides which side that surface hugs — user right, agent left — so a transcript is a VerticalLayout of bubbles and nothing has to align anything. max-surface-width is how wide the surface may grow before its text wraps.

text covers a single wrapped paragraph. @children carries everything else — a code block, a table, a custom layout — stacked under it inside the surface’s own padding. Slotted content that has to sit on the surface binds the three colors the bubble resolves (surface-color, edge-color, foreground) and the two lengths that carry its rhythm (padding-l, gap-l), the same way Card’s parts do. That is what keeps a slotted code block readable when the reader switches palette or theme mode.

The bubble is inline transcript content rather than an overlay, so it sits at elevation e2 — it is in the transcript, not floating over it.

Examples

Roles

role is who authored the turn. It picks the side the surface hugs and, while no variant overrules it, the tint too. It carries no behavior of its own. MessageRow takes the same property rather than an alignment of its own, so the frame and the surface inside it cannot disagree about which side a turn is on.

Variants

variant is a second axis, chosen independently of who authored the turn: a failed send is destructive whichever side it hugs, and an unframed answer is a ghost. default means “the role’s own tint”, so a transcript that never names a variant is unchanged (ADR-0034).

outline and ghost draw no fill, so a bubble in either sits directly on whatever the transcript’s background is.

Reactions

Reactions are a model rather than slotted content: the one @children is spent on the turn, and slotted content is laid out inside the surface’s padding where it could not overhang the edge a reaction anchors to (ADR-0034). So the strip is [string] plus the two properties that place it — reactions-side picks the edge it straddles, reactions-align the end of that edge it gathers at.

Each label is the consumer’s to compose — ”👍 3” in most chat applications, words in the previews here: the bubble counts nothing and knows nobody, and an emoji is only as available as the fonts the platform has. Each pill is a Button, and reaction-activated(index) reports which of them was taken.

A bubble that is a control

interactive makes the whole surface the control — the shape behind “open this turn”, “quote it”, “see the tool call behind it”. The control layer sits under the content, so a button the consumer slotted in still takes its own press and a press nothing above it wanted falls through to the surface: the trigger and the actions never trap each other.

A bubble that is not interactive has no control layer at all, so there is nothing for Tab to land on.

Slotted content on the surface

The three colors the bubble resolved are published, so content that has to sit on the surface reads them instead of restating the two axes. A code block under a user turn is on color-primary; the same block under an agent turn is on color-card, and neither call site had to know which.

API Reference

Properties

PropertyTypeDefaultDescription
rolein MessageRoleMessageRole.agentWho authored the turn — user hugs the right edge, agent the left.
textin stringno defaultThe turn's message text; empty string omits it and leaves the slot as the only content.
max-surface-widthin length480pxWidest the surface may grow before its text wraps — the row itself still spans the transcript.
variantin MessageBubbleVariantMessageBubbleVariant.defaultHow the surface is painted. default is the role's own tint.
reactionsin [string]no defaultThe reactions carried on the surface's edge, each a label the consumer has already composed ("👍 3"). Empty draws no strip.
reactions-sidein MessageReactionSideMessageReactionSide.bottomWhich edge of the surface the strip straddles, and which end of it the reactions gather at.
reactions-alignin MessageReactionAlignMessageReactionAlign.end
interactivein boolfalseMakes the whole surface the control: hover, press and the focus ring apply to the bubble, not to something nested inside it. A bubble that is not interactive has nothing for Tab to land on.
action-labelin stringroot.textWhat assistive technology announces the surface as while it is a control. The turn's own text says it for the common case; a bubble whose content is all slotted has to be told.
surface-colorout colorno defaultSurface fill, for slotted content that has to sit on it. The role decides it while the variant is default; every other variant is the consumer overruling the role, which is the whole point of the axis.
edge-colorout colorno defaultThe hairline around the fill. A filled surface is its own edge, and the hairline tokens are page-background overlays that would vanish against it in either theme mode — so only the surfaces that need lifting off the background get one.
foregroundout colorno defaultText color for slotted content, resolved from the same two axes.
padding-lout lengthno defaultSurface padding, for slotted content that draws to the bubble edge.
gap-lout lengthno defaultVertical rhythm between the message text and slotted content.

Callbacks

CallbackDescription
clicked()Fired when an interactive surface is activated, by pointer, by Enter or Space, or through the accessible default action.
reaction-activated(int)Fired with the place in reactions of the one that was activated.

Enums

EnumValues
MessageRoleuser, agent
MessageBubbleVariantdefault, secondary, muted, tinted, outline, ghost, destructive
MessageReactionSidetop, bottom
MessageReactionAlignstart, end

Accessibility

  • An inert bubble is text, not a control. The turn’s own text node is what a screen reader reads; nothing announces the surface, and Tab has nothing to land on. That is deliberate — an affordance that does nothing must not collect a tab stop.
  • interactive makes it a button, named by action-label — which defaults to text and has to be set where the turn’s content is all slotted, or the surface reads as an anonymous button.
  • One activation path. The pointer, Enter, Space and the accessible default action all land on the same clicked, so they cannot drift apart.
  • The control layer is under the content, so a slotted button keeps its own press and its own place in the tab order.
  • A reaction is a control, not a decoration. Each pill is a real Button: it is announced, it is reachable from the keyboard, and it reports its place in reactions.
  • Keyboard-only focus ring, around the surface when the keyboard put the focus there and not when a click did.
  • Color is never the only channel. destructive says a send failed with its tint; the words that say so belong in the turn’s own text, where assistive technology will find them.