MessageRow
The frame around a turn — the avatar gutter, the author-and-timestamp header, and the footer a turn's actions sit in.
import { MessageBubble, MessageRole } from "@glint/components/message-bubble.slint";import { MessageRow } from "@glint/components/message-row.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component Demo inherits Window { width: 560px; height: 300px; background: Tokens.color-background;
VerticalLayout { padding: 20px; spacing: 4px; alignment: center;
MessageRow { role: MessageRole.user; author: "You"; timestamp: "09:40"; avatar-fallback: "FM";
MessageBubble { role: MessageRole.user; text: "Where does the parser lose the span?"; } }
MessageRow { role: MessageRole.agent; author: "Ada"; timestamp: "09:41"; avatar-fallback: "AL";
MessageBubble { role: MessageRole.agent; text: "In `fold_expr` — it rebuilds the node without carrying the span over."; } } }}Usage
import { MessageBubble, MessageRole } from "@glint/components/message-bubble.slint";import { MessageRow } from "@glint/components/message-row.slint";
export component AppWindow inherits Window { VerticalLayout { MessageRow { role: MessageRole.agent; author: "Ada"; timestamp: "09:41"; avatar-fallback: "AL";
MessageBubble { role: MessageRole.agent; text: "Here's the diff."; } } }}MessageBubble is the surface a turn is said
on; this is everything around it. The two take the same role rather than
the row taking an alignment of its own, so the frame and the surface inside it
cannot disagree about which side a turn is on — one axis, spelled once at each
call site.
Slint gives a component one @children, and the row spends it on the content
column: the header and the avatar are properties, and what you slot in is the
bubble, optionally followed by a footer row. Both gutters are reserved
whichever one the avatar is standing in, which is what keeps a user turn and
an agent turn the same width.
There is no MessageGroup. A parent can neither inspect nor rewrite its
children in Slint, so a run of turns from one sender is grouped: true on
every row after the first, set by you.
Examples
Two columns of turns
role puts the avatar in the author’s own gutter and hugs the content column
to that side, so a transcript reads as two columns. The header stays at the
start of the content column either way: a right-aligned name over a
right-aligned bubble reads as part of the message rather than as who said it.
import { MessageBubble, MessageRole } from "@glint/components/message-bubble.slint";import { MessageRow } from "@glint/components/message-row.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component Demo inherits Window { width: 560px; height: 280px; background: Tokens.color-background;
VerticalLayout { padding: 20px; alignment: center;
MessageRow { role: MessageRole.agent; author: "Ada"; timestamp: "11:02"; avatar-fallback: "AL";
MessageBubble { role: MessageRole.agent; text: "The agent's avatar stands in the leading gutter."; } }
MessageRow { role: MessageRole.user; author: "Felipe"; timestamp: "11:03"; avatar-fallback: "FM";
MessageBubble { role: MessageRole.user; text: "And the user's in the trailing one."; } } }}Continuation rows
grouped: true is the second and later turns of a run from one sender: the
avatar and the header go, the gutter stays, and the row sits tighter under its
predecessor — so the run reads as one block, still flush with the turn that
introduced it.
import { MessageBubble, MessageRole } from "@glint/components/message-bubble.slint";import { MessageRow } from "@glint/components/message-row.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component Demo inherits Window { width: 560px; height: 300px; background: Tokens.color-background;
VerticalLayout { padding: 20px; alignment: center;
MessageRow { role: MessageRole.agent; author: "Ada"; timestamp: "11:02"; avatar-fallback: "AL";
MessageBubble { role: MessageRole.agent; text: "I split the change into three commits."; } }
MessageRow { role: MessageRole.agent; grouped: true;
MessageBubble { role: MessageRole.agent; text: "The first one only moves files."; } }
MessageRow { role: MessageRole.agent; grouped: true;
MessageBubble { role: MessageRole.agent; text: "The second is the one worth reading."; } } }}A footer under the turn
Slot a second element after the bubble and it becomes the row’s footer — the
actions or the delivery status under a turn. The avatar sits level with the
bubble rather than with the actions below it, and Slint gives a component no
way to measure a slotted child, so footer-height is the call site’s to
state: it is how far the row lifts the avatar off the bottom.
has-hover is the row noticing the pointer without taking it, so footer
actions can come forward on hover without each of them owning a TouchArea.
Bring them forward rather than making them appear: a control faded to nothing
is still in the tab order, and a keyboard user would land on something nobody
can see.
import { Button, ButtonSize, ButtonVariant } from "@glint/components/button.slint";import { MessageBubble, MessageRole } from "@glint/components/message-bubble.slint";import { MessageRow } from "@glint/components/message-row.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component Demo inherits Window { width: 560px; height: 260px; background: Tokens.color-background;
VerticalLayout { padding: 20px; alignment: center;
row := MessageRow { role: MessageRole.agent; author: "Ada"; timestamp: "11:07"; avatar-fallback: "AL"; footer-height: 28px;
MessageBubble { role: MessageRole.agent; text: "Hover the row — the actions under it come forward while the pointer is on it."; }
HorizontalLayout { spacing: 4px; alignment: start; // The footer follows the row's own hover, so neither button // needs a TouchArea to notice the pointer. It dims rather // than disappearing: a control at zero opacity still holds // its tab stop. opacity: row.has-hover ? 1.0 : 0.45;
Button { variant: ButtonVariant.ghost; size: ButtonSize.xs; text: "Copy"; } Button { variant: ButtonVariant.ghost; size: ButtonSize.xs; text: "Retry"; } } } }}Capping the bubble against the content column
content-max-width is the row less both gutters — what a slotted bubble
should cap against, rather than against the whole transcript. Bind
max-surface-width to it and the surface stops exactly where the content
column does, whatever the avatar size and the gutter add up to.
import { MessageBubble, MessageRole } from "@glint/components/message-bubble.slint";import { MessageRow } from "@glint/components/message-row.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component Demo inherits Window { width: 560px; height: 260px; background: Tokens.color-background;
VerticalLayout { padding: 20px; alignment: center;
row := MessageRow { role: MessageRole.agent; author: "Ada"; avatar-fallback: "AL"; avatar-size: 40px; gutter: 12px;
MessageBubble { role: MessageRole.agent; max-surface-width: row.content-max-width; text: "A long turn wraps at the edge of the content column rather than at the edge of the transcript, so the gutter on the far side stays empty and both turns keep the same width."; } } }}API Reference
Properties
| Property | Type | Default | Description |
|---|---|---|---|
role | in MessageRole | MessageRole.agent | Who authored the turn — the agent's frame keeps to the leading side, the user's to the trailing one. The same property the bubble takes. |
avatar-source | in image | no default | The author's picture, and the initials shown while there is none. |
avatar-fallback | in string | no default | |
avatar-size | in length | 32px | The avatar's diameter, and the gap between it and the content column. |
gutter | in length | Tokens.spacing-sm | |
grouped | in bool | false | A continuation of the turn above: the gutter stays, the avatar and the header go, and the row sits tighter under its predecessor. |
author | in string | no default | The header, leading the content column. An empty author leaves the header out even on a row that is not a continuation. |
timestamp | in string | no default | Trails the author, in the muted foreground. Consumer data — Glint formats no times. |
footer-height | in length | 0px | How tall the footer slotted under the bubble is. The avatar sits level with the bubble rather than with the actions below it, and Slint gives a component no way to measure a slotted child — so the call site states it. Zero is a row with no footer. |
content-max-width | out length | no default | How wide the content column is: the row less both gutters. A slotted bubble caps against this rather than against the whole transcript. |
has-hover | out bool | no default | Whether the pointer is anywhere on the row, so a consumer's footer actions can appear on hover without each of them owning a TouchArea. |
Enums
| Enum | Values |
|---|---|
MessageRole | user, agent |
Accessibility
- The row is presentational. It carries no role of its own: what is announced is the turn inside it, plus the header’s own text nodes.
- The avatar is decorative.
authoralready carries the identity, so the face leaves the accessibility tree entirely rather than standing in it as an unnamed image node. - The header is text. The author and the timestamp are read as they are written; Glint formats no times, so what a screen reader says is exactly the string you passed.
- A continuation row says nothing new.
grouped: truedrops the header along with the avatar, so a run of turns from one sender does not repeat the author on every one of them. - Nothing here takes the keyboard. The row owns no control, so it takes no tab stop of its own: what a keyboard reader walks is whatever you slotted into it — an interactive bubble, the footer’s buttons — in the order you wrote them.
- Hover is not focus.
has-hoveris a pointer fact. Footer actions driven off it keep their tab stops whether or not they are drawn, so fade them rather than hiding them — otherwise a keyboard user lands on a control that is not on screen.