Chat Timeline
A conversation with temporal structure — day headers that pin as you scroll, an unread divider, and collapsed avatars on consecutive messages from the same sender. Place it inside a Conversation.
Monday
Today
tsx
import {
Conversation,
ConversationContent,
} from "@/registry/seam/ui/conversation"
import {
Message,
MessageAvatar,
MessageContent,
} from "@/registry/seam/ui/message"
import {
ChatTimeline,
ChatTimelineGroup,
ChatTimelineHeader,
} from "@/registry/seam/ui/chat-timeline"
const DAYS = [
{
label: "Monday",
messages: [
{ from: "user", text: "Can you set up the project?" },
{ from: "assistant", text: "Done — registry, docs, and CI are wired." },
{ from: "assistant", text: "Want me to open a PR?" },
],
},
{
label: "Today",
messages: [
{ from: "user", text: "Yes, open it." },
{ from: "assistant", text: "Opened. Sticky headers pin as you scroll." },
],
},
] as const
export default function ChatTimelineDemo() {
return (
<div className="bg-card flex h-80 w-full max-w-md rounded-xl squircle border shadow-resting">
<Conversation>
<ConversationContent>
<ChatTimeline>
{DAYS.map((day) => (
<ChatTimelineGroup key={day.label}>
<ChatTimelineHeader>{day.label}</ChatTimelineHeader>
{day.messages.map((m, i) => {
// Collapse the avatar on consecutive assistant messages.
const prev = day.messages[i - 1]
const firstOfRun = !prev || prev.from !== m.from
return (
<Message key={i} from={m.from} role="listitem">
{m.from === "assistant" &&
(firstOfRun ? (
<MessageAvatar name="Seam UI" />
) : (
<span className="w-8 shrink-0" aria-hidden />
))}
<MessageContent>{m.text}</MessageContent>
</Message>
)
})}
</ChatTimelineGroup>
))}
</ChatTimeline>
</ConversationContent>
</Conversation>
</div>
)
}bunx --bun @seamui/cli@latest add chat-timelineNotes
- Grouping is presentational — you pass ordered messages and decide the day boundaries and which consecutive-sender runs collapse their avatar; the component supplies the layout and the sticky headers.
- The date chips pin with a constant subtle backdrop — nothing translates, so they read the same with reduced motion on. New groups inherit the Message entrance; regrouping is instant by design, with no layout springs — reflowing history must never wobble.
- The timeline is a
role="list"of message rows; the unread divider is a labeledrole="separator"announced once — the flanking rules are decorative. - Collapsed avatars leave a spacer so alignment holds without adding noise for screen readers.
Press feedback, reduced motion, and haptics follow the global policy — see Motion and Haptics.