@cf/message
Message
The row in a chat thread that holds one turn, aligned to the start or end, with optional avatar, header and footer around its bubbles.
Layout only: no state, no Base UI. Message is a flex row that reverses for align="end", and MessageContent pushes its slotted children to the end on those rows. MessageAvatar lifts itself above a footer when there is one.
In the chat panel every turn is Message then MessageContent then a Bubble, with a MessageFooter for the Sent and Delivered receipt. I don't use MessageAvatar or MessageHeader there yet.
import { Message, MessageContent, MessageFooter } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
<Message align="end">
<MessageContent>
<Bubble align="end" variant="outline">
<BubbleContent>{text}</BubbleContent>
</Bubble>
<MessageFooter>Delivered</MessageFooter>
</MessageContent>
</Message>Props
| Prop | Type | Default |
|---|---|---|
| Message align | start | end | start |
Rules
The same rules ship in DESIGN.md and the MCP server, for the agents building with this.
- must
Set
align="end"onMessagefor the person's own turns and leavealign="start"for everyone else, and set the samealignon theBubbleinside.align-by-speaker
- must
Put bubbles, quotes and attachments inside
MessageContent, not directly inMessage, so they stack and follow the row's alignment.content-wraps-bubbles
- should
Put timestamps and delivery receipts in
MessageFooter, which right-aligns itself onendmessages and drops its padding next to a ghost bubble.footer-for-receipts
- should
Wrap each Message in a
MessageScrollerItemwhen it's inside aMessageScroller, rather than the other way round.scroller-item-outside
Install
bunx shadcn@latest add @cf/messageNeeds the @cf registry in your components.json once. The theme and any sibling components come along automatically.
Source
message.tsxShowHide
import { cn } from "cn";
import type * as React from "react";
function MessageGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("flex min-w-0 flex-col gap-2", className)}
data-slot="message-group"
{...props}
/>
);
}
function Message({
className,
align = "start",
...props
}: React.ComponentProps<"div"> & { align?: "start" | "end" }) {
return (
<div
className={cn(
"group/message relative flex w-full min-w-0 gap-2 text-sm data-[align=end]:flex-row-reverse",
className
)}
data-align={align}
data-slot="message"
{...props}
/>
);
}
function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted group-has-data-[slot=message-footer]/message:-translate-y-8",
className
)}
data-slot="message-avatar"
{...props}
/>
);
}
function MessageContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"wrap-break-word flex w-full min-w-0 flex-col gap-2.5 group-data-[align=end]/message:*:data-slot:self-end",
className
)}
data-slot="message-content"
{...props}
/>
);
}
function MessageHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex min-w-0 max-w-full items-center px-3 font-medium text-muted-foreground text-xs group-has-data-[variant=ghost]/message:px-0",
className
)}
data-slot="message-header"
{...props}
/>
);
}
function MessageFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"flex min-w-0 max-w-full items-center px-3 font-medium text-muted-foreground text-xs group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end",
className
)}
data-slot="message-footer"
{...props}
/>
);
}
export {
Message,
MessageAvatar,
MessageContent,
MessageFooter,
MessageGroup,
MessageHeader,
};