Message
Message is the row-level composition primitive for a conversation. It owns the horizontal alignment and the vertical stack that contains optional sender identity, metadata, content, and footer slots. It does not own a sender model, timestamp formatting, delivery state, reaction state, or message actions.
Applications provide those values and compose existing components inside the slots. This keeps the message layout reusable across direct messages, group chat, assistant responses, system notices, and generated content.
Import
use gpui::{ParentElement as _, StyleRefinement, Styled as _};
use gpui_component::{
ActiveTheme as _, Colorize as _, Sizable as _,
attachment::{Attachment, AttachmentContent, AttachmentTitle},
avatar::Avatar,
bubble::{Bubble, BubbleVariant},
button::{Button, ButtonVariants as _},
message::{
Message, MessageAlignment, MessageAvatar, MessageContent, MessageFooter,
MessageGroup, MessageHeader,
},
};Anatomy and basic usage
All named slots are optional, so a minimal message can contain only a body:
Message::new().content(
MessageContent::new().bubble(Bubble::new().child("Can you review this?")),
)A complete message commonly combines sender identity, metadata, a bubble, and a delivery footer:
Message::new()
.avatar_slot(
MessageAvatar::new()
.child(Avatar::new().name("Alice").size_8()),
)
.header(
MessageHeader::new()
.child("Alice")
.child("10:24 AM"),
)
.content(
MessageContent::new().bubble(
Bubble::new()
.with_variant(BubbleVariant::Secondary)
.child("Can you review this draft?"),
),
)
.footer(MessageFooter::new().child("Read"))The default state is:
| Property | Default | Meaning |
|---|---|---|
| Alignment | MessageAlignment::Start | Place the message at the leading edge. |
| Avatar/header/content/footer | absent | Add only the slots needed by the product. |
| Outer layout | full width, min_w_0(), gap_2() | Keeps rows usable in a virtual list. |
| Inner stack gap | rems(0.625) | Separates metadata, body, and footer. |
| Header/footer inset | enabled, px_3() | Aligns metadata with a regular bubble surface. |
| Avatar baseline | min_w_8(), circular muted surface | Gives sender identity a stable column. |
Message applies its alignment to the complete row and to the named content stack. It reverses the outer row for End, so the avatar and message stack remain a single aligned unit.
Alignment
Use Start for incoming content and End for outgoing content:
Message::new()
.alignment(MessageAlignment::Start)
.avatar(Avatar::new().name("Alice").size_8())
.header(MessageHeader::new().child("Alice").child("10:24 AM"))
.content(MessageContent::new().bubble(
Bubble::new()
.with_variant(BubbleVariant::Secondary)
.child("Incoming message"),
));
Message::new()
.alignment(MessageAlignment::End)
.avatar(Avatar::new().name("You").size_8())
.header(MessageHeader::new().child("You").child("10:25 AM"))
.content(MessageContent::new().bubble(Bubble::new().child("Outgoing message")))
.footer(MessageFooter::new().child("Delivered"))MessageAlignment is also accepted by Bubble. When the body is a typed MessageContent::bubble(...), the message propagates its alignment to that bubble's surface. Leave the bubble's own alignment unset in this composition so the row has one clear owner of placement.
Avatar, header, content, and footer
Avatar
.avatar(...) wraps any element in MessageAvatar. Use .avatar_slot(...) when the slot itself needs styling or multiple children:
Message::new()
.avatar_slot(
MessageAvatar::new()
.bg(cx.theme().transparent)
.child(Avatar::new().name("System").size_8()),
)
.content(MessageContent::new().child("A system update"))The avatar reserves the shared size-8 baseline and always sits flush with the bottom edge of the message content; the footer renders below the avatar row, indented to the content column. The message does not require an avatar; omit it for assistant messages, compact group chat, or system rows where identity is already present elsewhere.
Header
MessageHeader is an arbitrary horizontal metadata row. It defaults to an extra-small, medium-weight, muted style with a px_3() inset:
MessageHeader::new()
.child("Alice")
.child("·")
.child("10:24 AM")The header does not format dates or infer sender names. Use application-owned formatting and compose a Tooltip around timestamps when the full date is useful.
Content
MessageContent is a full-width, minimum-width-safe vertical stack. It accepts arbitrary elements and has a typed .bubble(...) convenience that records whether a ghost bubble is present:
MessageContent::new()
.bubble(Bubble::new().child("First paragraph"))
.bubble(Bubble::new().child("Second paragraph"))Use .child(...) for attachments, code blocks, images, or custom rich content:
MessageContent::new()
.bubble(Bubble::new().child("Here is the file:"))
.child(
Attachment::new().content(
AttachmentContent::new()
.title(AttachmentTitle::new("quarterly-report.pdf")),
),
)Typed bubbles are useful when the surrounding header and footer should respond to the Ghost variant. Arbitrary .child(...) values are still fully composable, but their concrete type is erased and they do not set that ghost-surface metadata.
Footer
MessageFooter is another arbitrary horizontal metadata row. Use it for delivery state, reactions, or actions composed from existing controls:
MessageFooter::new()
.child("Delivered")
.child(Button::new("reply").ghost().xsmall().label("Reply"))
.child(Button::new("copy").ghost().xsmall().label("Copy"))Footer uses the same extra-small muted default and px_3() inset as the header. The footer does not own a delivery-state enum or action semantics.
Rich content and actions
Compose the existing component that owns each behavior:
Message::new()
.content(
MessageContent::new()
.bubble(Bubble::new().child("The export is ready."))
.child(
Attachment::new()
.content(AttachmentContent::new().title(AttachmentTitle::new("export.zip"))),
),
)
.footer(
MessageFooter::new()
.child(Button::new("download-export").label("Download"))
.child(Button::new("share-export").ghost().label("Share")),
)Use Button for commands, Link for URLs, Attachment for files, and Bubble for conversational surfaces. This keeps disabled, loading, focus, keyboard, and accessible-name behavior on the control that owns it. A message does not become clickable merely because it contains a button.
Long or multiline content remains the responsibility of the child element. Keep custom children min_w_0() when they contain long text or horizontal layouts; Message already applies w_full() and min_w_0() to its own row and stack.
Grouping
MessageGroup is a styleable vertical stack for consecutive messages. It does not decide which sender owns a message or automatically remove metadata:
MessageGroup::new()
.child(
Message::new()
.avatar(Avatar::new().name("Alice").size_8())
.header(MessageHeader::new().child("Alice"))
.content(MessageContent::new().bubble(
Bubble::new()
.with_variant(BubbleVariant::Secondary)
.child("The first message."),
)),
)
.child(
Message::new()
.avatar_slot(MessageAvatar::new().bg(cx.theme().transparent))
.content(MessageContent::new().bubble(
Bubble::new()
.with_variant(BubbleVariant::Secondary)
.child("The follow-up keeps the same sender context."),
)),
)Use BubbleGroup when only the bubbles are grouped and there is no message header, avatar, or footer. Use MessageGroup when each item is a full row.
Ghost surfaces and content insets
The typed MessageContent::bubble(...) builder records a ghost bubble. In that case, Message removes the default header and footer insets so metadata lines up with the unframed content:
Message::new()
.header(MessageHeader::new().child("System").child("Just now"))
.content(MessageContent::new().bubble(
Bubble::new()
.with_variant(BubbleVariant::Ghost)
.child("The conversation has been archived."),
))
.footer(MessageFooter::new().child("No further action required"))Override this behavior explicitly on either named metadata slot:
MessageHeader::new()
.content_inset(true)
.child("Keep the regular header inset");
MessageFooter::new()
.content_inset(false)
.child("Align the footer with a custom surface")content_inset(...) takes precedence over inherited ghost behavior. A typed ghost bubble is required for automatic inheritance; an arbitrary child that happens to look like a ghost surface cannot be inspected by Message.
The inner slot stack can also be refined independently:
Message::new()
.with_stack_style(StyleRefinement::default().gap_3())
.content(MessageContent::new().child("A wider message rhythm"))Custom styling and theme tokens
Message, MessageGroup, MessageAvatar, MessageHeader, MessageContent, and MessageFooter implement Styled. Style the part that owns the visual decision:
Message::new()
.p_3()
.rounded(cx.theme().radius_lg)
.bg(cx.theme().muted.opacity(0.35))
.header(MessageHeader::new().px_0().child("System"))
.content(MessageContent::new().child("Archived"))
.footer(MessageFooter::new().px_0().child("Just now"))Use with_stack_style(...) for the vertical stack, slot refinements for header/content/footer typography and spacing, and the child component's own API for bubble, attachment, or button surfaces. Radius, spacing, typography, and colors should come from the active semantic theme or shared scale. Avoid raw colors at message call sites so the same composition works in light and dark themes.
Accessibility and state guidance
- Keep sender identity and message content in readable text. An avatar alone should not be the only indication of who sent a message.
- Put commands in semantic
ButtonorLinkcontrols. For the currentButtonAPI, use a visible.label(...)when a footer action needs an accessible name; a tooltip is supplemental. - Delivery, failure, streaming, and unread states belong in text or semantic controls. Do not communicate them with alignment, color, or opacity alone.
- Preserve the header/footer inset when it is the visual relationship that aligns metadata with the surface. If a custom surface removes it, verify the reading order and keyboard order still match the visual order.
- Keep multiline content readable at the application's minimum window width; use
min_w_0()on nested horizontal content and avoid hover-only actions. - Motion for generated content belongs to
ShimmerTextor another motion-aware component. Reduced-motion behavior should leave the message text present and understandable.
Component boundaries
The GPUI component intentionally does not add provider or domain layers:
Messageowns row alignment and slot layout.- The application owns sender records, timestamps, delivery state, reactions, permissions, message IDs, and persistence.
Bubble,Attachment,Button,Link, andMarkerown their own visual or behavioral primitives and are composed through message slots.MessageGrouponly supplies a vertical stack. It does not infer sender changes or collapse headers.
If a product needs a specific “assistant message” or “group chat message” with fixed metadata policy, wrap Message in an application component. Keep that domain policy out of the general-purpose primitive.
API reference
Message
| Method | Default | Purpose |
|---|---|---|
new() | Start, no slots | Create a message row. |
alignment(MessageAlignment) | Start | Set leading or trailing alignment. |
with_stack_style(StyleRefinement) | component stack defaults | Refine the inner vertical stack. |
avatar(element) | none | Wrap an element in MessageAvatar. |
avatar_slot(MessageAvatar) | none | Set a fully configured avatar slot. |
header(MessageHeader) | none | Set sender and metadata content. |
content(MessageContent) | none | Set the message body. |
footer(MessageFooter) | none | Set delivery, reactions, or actions. |
Message also implements Styled for the outer row.
MessageGroup
| Method | Default | Purpose |
|---|---|---|
new() | empty vertical stack | Create a message group. |
.child(element) | — | Add complete messages. |
Styled methods | gap_2() | Refine group spacing and layout. |
MessageAvatar
| Method | Default | Purpose |
|---|---|---|
new() | empty circular size_8 baseline | Create an identity slot. |
.child(element) | — | Add Avatar or another identity element. |
Styled methods | muted surface and full radius | Refine size, background, and alignment. |
MessageHeader and MessageFooter
| Method | Default | Purpose |
|---|---|---|
new() | empty extra-small metadata row | Create the slot. |
content_inset(bool) | inherited or true | Keep or remove the default px_3() inset. |
.child(element) | — | Add text, metadata, reactions, or controls. |
Styled methods | muted, medium-weight, text_xs() | Refine the slot. |
MessageContent
| Method | Default | Purpose |
|---|---|---|
new() | empty full-width vertical stack | Create the body slot. |
bubble(Bubble) | — | Add a typed bubble and propagate ghost metadata. |
.child(element) | — | Add arbitrary rich content. |
Styled methods | min_w_0(), gap(rems(0.625)) | Refine body layout. |
Related types
- [
MessageAlignment] —StartorEnd. - [
Bubble] — conversational surface content. - [
Attachment] — files and media. - [
MessageScroller] — virtualized conversation rows and tail following.