Bubble
The message surface โ seven variants, two alignments, and a row of reactions that hangs off the edge.
src/components/rahti_ui/bubble.rs
Usage
A bubble, its content, and a reaction hanging off the corner.
shadcn's Usage example. A bubble is as wide as its text up to four fifths of the row โ `w-fit max-w-[80%]` โ so a one-word reply is a small pill and a paragraph fills most of the line.
The reaction row is absolutely positioned and hangs three quarters of its height below the bubble, cut out of it by a ring in the card's colour rather than a border. Leave vertical room between rows; this page uses gap-3 where the rest of the site would use gap-2.
Variants
Seven treatments, from a strong primary bubble to unframed text.
All seven. None of them styles the bubble it is written on โ every variant is a rule aimed at the content, in the shape of a direct-child variant. So a BubbleContent has to be a *direct* child; one wrapper in between and all seven switch off.
Ghost is the odd one: it drops the radius, the background and the padding, and the root gives up the max-width by reading its own data-variant. That is what makes it right for assistant markdown, which should not look like it was sent by anyone.
Alignment
start for the other person, end for the reader.
align on the Bubble itself moves it with self-end. Inside a Message, though, leave it alone and set align on the row โ the bubble already reads the row's through group-data-[align=end]/message:self-end, and shadcn's own note says to put it there.
Bubble group
Consecutive bubbles from one sender, stacked tighter.
BubbleGroup stacks a run from one sender at gap-2, tighter than the gap-3 between the two groups. It is the same six utilities MessageGroup uses, for the same reason.
align goes on each Bubble, not on the group โ shadcn says so, and the group has no alignment rule of its own to apply.
Links and buttons
render, and the four rules that were waiting for it.
Hover the last three and tab through them. `render` turns the content into a real button or anchor, so it is focusable and announces the right role, and its accessible name is its own text โ nothing else to write.
The focus ring, the colour transition and the left-aligned label are already in the content's own string as [button,a]: rules, and each variant brings a hover colour for the same two elements. That is why render and the variants are really one feature seen twice.
shadcn's prop takes a React element and this one takes an element name. There is nothing to pass a component tree here, so the prop keeps its name and narrows to the three elements a bubble is ever rendered as. A render="button" is given type="button", so a bubble inside a form cannot submit it.
Reactions
A row pinned to any corner, overlapping the edge.
side is top or bottom and align is start or end, so a row can hang off any of the four corners. The third bubble uses the upper-start corner; the first two take the defaults, which are bottom and end โ note that a Bubble defaults to align=start and its reactions default to align=end. Two different questions under one prop name.
Every row here is one image with a name. A screen reader otherwise reads each glyph on its own and says "plus two" for the counter; role="img" also hides the glyphs, so none of them needs aria-hidden.
Inside a Message
The composition both components were built for.
Neither bubble carries an align. The row writes data-align and the bubble reads it back through group-data-[align=end]/message:self-end โ which is the whole of what Bubble knows about a conversation, and the reason shadcn says to put alignment on the Message.
Bubble is what the Message and Message Scroller demo pages were standing in for. Both of them now render this component instead of a placeholder holding its strings, and nothing on either page had to change to make the ghost padding rule start working.
Show more
Composed with Collapsible, which needed nothing from Bubble.
Long content composes with Collapsible, which is shadcn's own suggestion and needed nothing from this component โ a bubble is a box with text in it, and the box grows.
The docs page ends on three of these: Collapsible for show-more, Tooltip for read receipts, Popover for the full text of a failed action. All three are already in this library, and none of them is a Bubble prop.
Accessibility
Meaning beyond colour, and the two things Bubble asks of a caller.
A variant signals role and tone with colour, so pair it with something else. The destructive bubble above says what failed in its own text and carries an icon; the colour is the third signal rather than the only one.
Bubble renders the message surface and nothing about the conversation โ no roles, no live region, no labels. Those belong to Message and Message Scroller. The two things this component does ask for are a name on a reaction row and a real button or anchor for an interactive bubble, and both are demonstrated above.
The port
What moved, what did not, and why.