Message
A conversation's bubbles: runs from one sender on their side, time separators between pauses, and the dots while the other side works.
import { Bubble, MessageGroup, TimeSeparator, … } from "@arcadia/ui"A conversation is runs of bubbles. Consecutive messages from one sender share a MessageGroup, which stacks them --message-gap (2px) apart on their side; give each bubble its position from bubblePosition(i, n) and the corners that face a neighbour tighten to --bubble-tight, so the run reads as one voice. The end side is you: a step of ink over the raised surface, ink text. The start side is the other party on the raised surface, no frame: a solid bubble's fill is its edge. variant="outline" is a hairline and no fill, for a message relayed from someone else. Space runs apart with the conversation's own gap.
Put a TimeSeparator before the first message and wherever the conversation resumes after a pause. While the other side works, end its run with a TypingIndicator, where its next bubble will land.
Examples
rootStyle takes where a bubble sits and these knobs: its material as palette tokens, its corners as radius tokens, its measure, padding and enter delay.
| Knob | Takes | Default | Sets |
|---|---|---|---|
Paint | var(--bg-raised) | The fill. start reads the raised surface, end a step of ink over the page. | |
Paint | var(--bg-elevated) | The fill while the bubble is selected (its menu is open). | |
Paint | var(--text-prose) | Text colour. | |
Paint | transparent | The hairline drawn inside the bubble: none on a solid bubble (its fill is its edge), a frame on an outline one. | |
Ref<Radius> | var(--radius-2xl) | The outer corners. | |
Ref<Radius> | var(--radius-sm) | The corners that face a neighbour in the same run. | |
`${number}% | Ref<Measure>` | 80% | Widest a bubble grows, as a share of its column. | |
Ref<Space> | var(--space-3) | Inline padding. | |
Ref<Space> | var(--space-1-5) | Block padding. | |
Ref<Duration> | "0s" | 0s | How long the enter waits before it plays (the bubble stays hidden until then): a reply's typing bubble waits for the sent bubble to land. |
| Knob | Takes | Default | Sets |
|---|---|---|---|
Ref<Space> | Ref<Space>; | The gap between bubbles of one run. | |
Ref<Control | Space> | Ref<Control | Space>; | The avatar column's width (start side, with an avatar). | |
Paint | Paint; | The name and caption colour. |
| Knob | Takes | Default | Sets |
|---|---|---|---|
Ref<Space> | Ref<Space>; | One dot's diameter. | |
Paint | Paint; | The dots' ink. |
A MessageGroup is a role="group"; name it with aria-label ("Juniper", "You") when the sender is not otherwise read. Bubble text stays selectable and wraps anywhere, so a long path never overflows its column.
TypingIndicator is an image named by label, or hidden when a live region already announces the work. The dots never carry meaning by colour, and reduced motion shows them still. Entry motion is skipped under reduced motion and on a hydrated page (data-instant).
The TimeSeparator is a time element; pass dateTime so assistive tech and copy carry the exact moment.
Bubble
side- "start" | "end"Default"start"
position- "single" | "first" | "middle" | "last"Default"single"
- Compute it with
bubblePosition(index, count). variant- "solid" | "outline"Default"solid"
enter- BubbleEnter | null
- Plays once on mount. Omit for bubbles that were already there (a reload, a scroll back).
flush- boolean
- No padding, its content clipped to the bubble's corners: a file card, a list of rows, media.
selected- boolean
- Its context menu is open: the fill steps up.
rootStyle- BubbleStyle
- Typed StyleX override: layout and the
--bubble-*knobs.
Rest props go to <div> props and render.
side- "start" | "end"Default"start"
avatar- ReactNode
- The sender's picture, beside the run's last bubble (start side only).
name- ReactNode
- The sender's name over the first bubble: for a party who isn't the conversation's own (another agent).
meta- ReactNode
- A quiet caption under the run, on its side: a receipt, a time.
rootStyle- MessageGroupStyle
- Typed StyleX override: layout and the
--message-*knobs.
Rest props go to <div> props.
day- ReactNode
- The day, in the stronger rank ("Today", "Yesterday", "Monday", "Oct 3").
time- ReactNode
- The time of day ("12:04 AM").
dateTime- string
- Machine-readable moment for the
<time>element (an ISO string). rootStyle- RootStyle
- Typed StyleX override: layout.
Rest props go to <div> props.
label- string
- Accessible name ("Juniper is working"). Omit when a live region already says so.
rootStyle- RootStyle<BubbleKnobs & TypingIndicatorKnobs>
- Typed StyleX override: layout, the
--bubble-*and--typing-*knobs. side- "start" | "end"Default"start"
position- "single" | "first" | "middle" | "last"Default"single"
- Compute it with
bubblePosition(index, count). enter- BubbleEnter | null
- Plays once on mount. Omit for bubbles that were already there (a reload, a scroll back).
selected- boolean
- Its context menu is open: the fill steps up.
Rest props go to <div> props and render.