Message
Lays out one chat message: an avatar beside a column of bubbles, cards, and actions, with a small name line above and a status line below. Bubbles come in seven fills and can carry a pill of reactions.
Usage
import {
Message,
MessageAvatar,
MessageBubble,
MessageContent,
MessageFooter,
MessageGroup,
MessageHeader,
} from "@tulipfarm/sprout/message";
<MessageGroup>
<Message align="start">
<MessageAvatar>
<Avatar><AvatarFallback>AM</AvatarFallback></Avatar>
</MessageAvatar>
<MessageContent>
<MessageHeader>Alex Meadow · 10:30</MessageHeader>
<MessageBubble variant="secondary">Can you help plan the garden?</MessageBubble>
<MessageFooter>Delivered</MessageFooter>
</MessageContent>
</Message>
<Message align="end">
<MessageContent>
<MessageBubble>Yes. Let's start with the planting list.</MessageBubble>
<MessageBubble>Tomatoes and basil go in the south bed.</MessageBubble>
</MessageContent>
</Message>
</MessageGroup>Put bubbles, text, cards, or buttons inside MessageContent. Consecutive bubbles from one sender go in the sameMessageContent.
align is "start", the default, or "end". An end message puts the avatar on the trailing side and moves every direct child of MessageContent that has a data-slot to the end. A child without a slot, such as a plain div, keeps its own width. The footer text also moves to the end. There are no sizes: text is 14px, the header and footer are 12px.
MessageGroup stacks messages 8px apart. Inside a message, the avatar and content are 8px apart and the content's children are 10px apart. MessageAvatar is at least 32px wide and sits at the bottom of the message. When the content ends with a MessageFooter, the avatar moves up so it sits beside the last bubble.
The header and footer are inset 12px so they line up with padded bubble text. When a direct child of the content has data-variant="ghost", a bubble without padding, the inset drops to 0.
Every part takes the props of a div, including ref, so you can read the rendered bubbles from MessageContentfor copy actions.
Bubbles
<Message align="end">
<MessageContent>
<MessageBubble variant="secondary">
Create a Customers table with Name, Email, Company, Phone, and Status.
</MessageBubble>
</MessageContent>
</Message>
<Message>
<MessageContent>
<MessageBubble variant="ghost">{answer}</MessageBubble>
</MessageContent>
</Message>MessageBubble is at most 80% of its column and as wide as its text. There is one size: 14px text with 8px by 12px padding and the panel radius. A bubble has no side of its own. It moves to the end insideMessage align="end", or inside any element withdata-slot="message" and data-align="end".
variant sets the fill. "default" uses the primary color for your own messages. "secondary" is for prompts sent to an agent. "muted" is a lighter fill for quiet notes. "tinted" mixes in the primary color so it follows a brand theme. "outline" draws a border for quoted text."ghost" drops the fill and padding and may span the full width, for agent answers. "destructive" marks a message that failed.
Reactions
import { MessageBubble, MessageReactions } from "@tulipfarm/sprout/message";
<MessageBubble
variant="secondary"
reactions={<MessageReactions align="start" side="bottom">🌱 2</MessageReactions>}
>
Hello from Alex.
</MessageBubble>Pass a MessageReactions to the reactions prop. It pins a pill over the "top" or "bottom" edge, 12px in from the "start" or "end" corner. The defaults are side="bottom" and align="end". With reactions, the bubble and pill are wrapped in an element withdata-slot="message-bubble-frame".
Leave room around a bubble with reactions. The pill hangs three quarters outside it. Its 3px ring uses --sprout-message-reactions-ring, which defaults to the surface color. Set it to the background behind the bubble.
Links and buttons
<MessageBubble variant="outline" render={<a href="/results/investor-update" />}>
Open result · Investor update draft
</MessageBubble>
<Message align="end">
<MessageContent>
<MessageBubble
variant="destructive"
render={<button type="button" />}
onClick={retry}
>
Couldn't send. Retry
</MessageBubble>
</MessageContent>
</Message>render swaps the bubble element. A link or button keeps the variant fill, gets a slightly stronger fill on hover, and shows a focus ring when reached with the keyboard. Reactions sit outside the bubble element, so a bubble rendered as a button can still hold reaction buttons. Buttons inside MessageReactions become 24px toggles. Set aria-pressed and an aria-label with the count.
Every bubble has data-slot="message-bubble", so a copy action can collect the text of each bubble in a message.
Reply status
<Message align="end">
<MessageContent>
<MessageBubble variant="secondary">{text}</MessageBubble>
<MessageFooter>
<p role="status"><Danger aria-hidden="true" /> The reply didn't finish.</p>
<Button size="sm" variant="outline" onClick={retry}>Retry reply</Button>
</MessageFooter>
</MessageContent>
</Message>
<Message align="start" aria-busy="true">
<MessageContent>
<span className="sr-only">TulipFarm said</span>
{streamedText}
</MessageContent>
</Message>Accessibility
Message and its bubbles add no role and are not focusable. Put messages in a list or a labelled region and let links, buttons, and bubbles rendered as either take focus in reading order. Make a scrolling conversation a focusable, labelled region so it can be scrolled with the keyboard. When a reply has no visible name, add visually hidden text such as "TulipFarm said". Mark a reply that is still streaming with aria-busy, and announce status changes with role="status" or role="alert".
Colors and fills change in 120ms and stop when reduced motion is requested. In forced colors mode every filled bubble and reaction pill gets a border. The layout uses logical sides, so end messages flip in right-to-left text.
Theme tokens
The header and footer use --sprout-muted. The avatar slot uses --sprout-secondary behind images that do not fill it. Text uses --sprout-text, --sprout-text-sm, and--sprout-text-xs.
Bubble fills use --sprout-primary,--sprout-secondary, --sprout-surface,--sprout-line, and --sprout-danger-subtle. Radius follows --sprout-radius-panel. The reaction ring uses--sprout-message-reactions-ring.