Message
A conversation row — avatar, alignment, header and footer around a surface it does not draw.
src/components/rahti_ui/message.rs
Usage
One row: a gutter, a column, and a surface the component does not provide.
shadcn's Usage example, part for part. The row is a flex line with a 32px avatar gutter and a column beside it; everything you can actually see is the placeholder inside MessageContent.
The composition the docs page draws: Message holds a MessageAvatar and a MessageContent, and the content holds a MessageHeader, the surface, and a MessageFooter — in that order. MessageGroup stacks whole rows.
Alignment
start for the other person, end for the reader.
align is start or end, and only the row takes it. The avatar does not move itself — the row writes data-align and reverses its own flex direction, and the other parts read the attribute back through group/message.
That is why nothing else in this component has an align prop: three of the five remaining parts already know, and the two that do not have nothing to do with the side.
Group
Consecutive messages from one sender, under one avatar.
MessageGroup stacks consecutive messages from one sender at gap-2 instead of the gap-4 this page puts between separate rows. The first two rows carry an empty MessageAvatar, which is shadcn's own instruction — min-w-8 holds the gutter open, so the run stays lined up under the one face that is drawn.
A self-closing tag is all an empty one needs. It renders a div with no children, which is 32 pixels wide and no pixels tall.
Header and footer
A name above, a status below, and what the footer does to the avatar.
The header is a sender's name; the footer is a status. Both are text-xs muted captions with a px-3 inset, which is what lines them up with the surface's own horizontal padding.
Look at the second avatar. A footer in the row shifts it up by 2rem, so it sits beside the message rather than beside the caption under it — and that is measured from the footer's data-slot, not from a prop. Take the MessageFooter away and the avatar drops back down.
The footer also follows the message to the far side: its one utility that the header does not have is group-data-[align=end]/message:justify-end.
Actions
Copy, retry, and a delivery failure — all of it in the footer.
Message-level actions go in the footer. Both rows have one, so both avatars are lifted; the second row's footer is pushed to the end because the row is.
Every icon-only button carries an aria-label. shadcn's accessibility section asks for it and nothing about this component can supply one — Message is a presentational wrapper, and the docs page says exactly that.
Attachment
An image and a file beside their messages, aligning themselves off the row.
shadcn's Attachment example, row for row: an image above a sent bubble, a file card under a received one, and a short reply. No avatars anywhere — this is the one demo on the docs page that leaves the gutter out entirely, so the content column is the full width of the conversation.
This is the clearest thing on the page about how MessageContent works. The image card is not told which side it is on; it carries a data-slot, the row says data-align of end, and star-data-slot self-end does the rest. The file card in the middle row is the same markup on a start row and stays left.
Both cards are real Attachments now, as are the bubbles beside them. Their download button is an AttachmentAction, which is this library's Button at ghost and icon-xs — shadcn's own card is built against a newer Button generation that is not ported, so the focus ring here is the one every other button on the site has rather than a second shape.
The ghost hook
A surface with no background of its own, and the caption inset that follows it.
The captions in the first row start flush with the text; the ones in the second are inset by 12px. That is one selector on the header and the footer — group-has-data-[variant=ghost]/message:px-0 — reading a data-variant that something inside the row sets.
data-variant is Bubble's, and a real Bubble is what sets it here. The selector was ported and kept live before Bubble existed, on the argument that a caller could set the attribute by hand in the meantime — and it started working the day bubble.rs landed, with nothing changed on this side.
Where the docs and the strings disagree
A header on an align=end row, which the docs page never draws.
shadcn's API reference says of MessageHeader: it stays aligned to the start regardless of align. Its class strings say otherwise, and this row is the proof — the header is on the right.
MessageContent carries group-data-[align=end]/message with a star-data-slot self-end, which reaches every direct child that has a data-slot. The header has one, so it moves with everything else. There is no demo of this case on the docs page: it renders exactly one MessageHeader, on an align of start.
The strings are what ships, so the strings are what is ported. If you want shadcn's documented behaviour instead, put self-start in the header's class — twMerge resolves it against the inherited self-end in the browser.
Long content
What min-w-0 and wrap-break-word are for.
min-w-0 is on four of the six parts and wrap-break-word is on the content. Together they are why an unbreakable string wraps inside the column instead of stretching the conversation wider than its container.
wrap-break-word is Tailwind's rename of break-words. This project builds 4.3.3, so the new spelling is the live one and the old one would be a class that never gets emitted.
Accessibility
Nothing here has a role, and that is the design.
Message is a presentational layout wrapper — six divs, no roles, no labels. shadcn's accessibility section says so outright, and this port adds nothing: accessibility comes from what you put inside.
Two things the docs page asks for. Give every icon-only action in a MessageFooter an aria-label, which the Actions section above does. And for an in-progress message, wrap the status in something with a role of status so a screen reader announces it as it appears — shadcn reaches for Marker there, and so does the row above: a real Marker with a Spinner in its icon slot, standing in a Message row with no bubble at all.
The port
What moved, what did not, and why.