Message
A conversation's bubbles: runs from one sender on their side, an author line for anyone else, and time separators between pauses.
import { Bubble, MessageGroup, TimeSeparator and 1 more } from "@arcadia/ui"A conversation is runs of bubbles. Consecutive messages from one sender share a MessageGroup, which stacks them --message-gap (4px) 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.
There is no avatar column: every bubble on a side starts at the same edge. When someone other than the conversation's own party speaks, author puts a 16px avatar and a name on one line over the run, on the bubble's text edge.
Put a TimeSeparator before the first message and wherever the conversation resumes after a pause. While the other side works, an Activity row stands where its next bubble will land, saying what it is doing in a few words.
Examples
Measure
A bubble grows to --bubble-max-w of its column and never past --bubble-cap (560px).
In a wide column the far side of every bubble stays clear, so two speakers read as two sides and never as one ragged block.
Sending
enter plays once on mount: send rises from the composer and grows from its corner on a spring.
Leave it off for bubbles a reload or a scroll brings back. --bubble-enter-delay holds an entrance back.
Arriving
arrive is Enter: it rises --enter-offset and fades in over --duration-enter on --ease-enter.
land is the reply that follows an activity row: it also grows from its bottom start corner and waits for the row to leave. send rises twice as far on the release spring.
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.
--bubble-bg- PaintDefault var(--bg-raised)
- The fill.
startreads the raised surface,enda step of ink over the page. --bubble-bg-selected- PaintDefault var(--bg-elevated)
- The fill while the bubble is selected (its menu is open).
--bubble-fg- PaintDefault var(--text-prose)
- Text colour.
--bubble-edge- PaintDefault transparent
- The hairline drawn inside the bubble: none on a solid bubble (its fill is its edge), a frame on an outline one.
--bubble-radius- Ref<Radius>Default var(--radius-2xl)
- The outer corners.
--bubble-tight- Ref<Radius>Default var(--radius-md)
- The corners that face a neighbour in the same run.
--bubble-max-w- `${number}%` | Ref<Measure>Default 80%
- Widest a bubble grows, as a share of its column.
--bubble-cap- Ref<Measure> | "100%"Default var(--bubble-measure)
- The measure no bubble passes, however wide its column: two sides then never overlap by much.
--bubble-px- Ref<Space>Default var(--space-3)
- Inline padding.
--bubble-py- Ref<Space>Default var(--space-1-5)
- Block padding.
--bubble-enter-delay- Ref<Duration> | "0s"Default 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.
--message-gap- Ref<Space>Default var(--space-1)
- The gap between bubbles of one run.
--message-meta-fg- PaintDefault var(--text-3)
- The author line's and the caption's colour.
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.
Entry motion is skipped under reduced motion and on a hydrated page (data-instant); under reduced motion a flash is a still ring for a shorter run.
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.
flash- boolean
- A jump just landed on it (a find hit, a link, a reply's source): a ring comes up, holds and fades. Set it for the run of
--duration-flash, then clear it. rootStyle- BubbleStyle
- Typed StyleX override: layout and the
--bubble-*knobs.
Rest props go to <div> props and render.
side- "start" | "end"Default "start"
author- ReactNode
- Who speaks, on one line above the first bubble: for a party who isn't the conversation's own (another agent). A 16px avatar and a name; it sits on the bubble's text edge, so every bubble of the conversation keeps one edge.
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.