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
Can you set up the project?
SU
Done — registry, docs, and CI are wired.
Want me to open a PR?
Today
Yes, open it.
SU
Opened. Sticky headers pin as you scroll.
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-timeline

Notes

  • 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 labeled role="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.