# Direct Message Thread

> One-to-one chat conversation with a contact header, sender-aligned message bubbles, a date marker, auto-follow scrolling, and a working composer.

A shadcn/ui block from the [@7ovr](https://7ovr.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/7ovr/chat-1).

- Page, with a live preview: https://shadcn.exchange/r/7ovr/chat-1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/7ovr/chat-1-light-7f656e8e33d43742.png
- Registry item: https://7ovr.com/r/chat-1.json
- Categories: chat
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://7ovr.com/r/chat-1.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@base-ui/react`
- Registry items: `avatar`, `bubble`, `button`, `input`, `marker`, `message`, `message-scroller`

## Files

### `registry/blocks/chat/1/chat-block.tsx`

```tsx
"use client"

import * as React from "react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Marker, MarkerContent } from "@/components/ui/marker"
import { Message, MessageContent, MessageFooter } from "@/components/ui/message"
import {
  MessageScroller,
  MessageScrollerButton,
  MessageScrollerContent,
  MessageScrollerItem,
  MessageScrollerProvider,
  MessageScrollerViewport,
} from "@/components/ui/message-scroller"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

type ChatMessage = {
  id: number
  from: "me" | "them"
  text: string
  time: string
}

const initialMessages: ChatMessage[] = [
  {
    id: 1,
    from: "them",
    text: "Hey! Did you get a chance to look at the new onboarding flow?",
    time: "9:41 AM",
  },
  {
    id: 2,
    from: "me",
    text: "Just went through it. The empty states are a really nice touch.",
    time: "9:42 AM",
  },
  {
    id: 3,
    from: "them",
    text: "Glad you think so. Anything you would change before we ship?",
    time: "9:43 AM",
  },
]

const contact = {
  name: "Mara Lin",
  role: "Product Designer",
  avatar: "https://i.pravatar.cc/80?img=32",
}

export default function ChatBlock() {
  const [messages, setMessages] = React.useState(initialMessages)
  const [draft, setDraft] = React.useState("")
  const nextId = React.useRef(initialMessages.length + 1)

  const send = (e: React.FormEvent) => {
    e.preventDefault()
    const text = draft.trim()
    if (!text) return
    const id = nextId.current++
    setMessages((prev) => [...prev, { id, from: "me", text, time: "Now" }])
    setDraft("")
  }

  return (
    <section className="flex w-full items-center justify-center bg-muted/30 px-6 py-16 text-foreground">
      <MessageScrollerProvider autoScroll defaultScrollPosition="end">
        <div className="flex h-[560px] w-full max-w-md flex-col rounded-lg border border-border bg-background">
          <div className="flex items-center gap-3 border-b border-border p-4">
            <Avatar className="size-9">
              <AvatarImage
                src={contact.avatar}
                alt={contact.name}
                className="grayscale"
              />
              <AvatarFallback>{contact.name[0]}</AvatarFallback>
            </Avatar>
            <div className="flex flex-col">
              <span className="text-sm font-semibold">{contact.name}</span>
              <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
                <span
                  className="size-1.5 rounded-full bg-success"
                  aria-hidden="true"
                />
                Online
              </span>
            </div>
          </div>

          <MessageScroller className="flex-1">
            <MessageScrollerViewport>
              <MessageScrollerContent className="gap-4 p-4">
                <Marker variant="separator">
                  <MarkerContent>Today</MarkerContent>
                </Marker>
                {messages.map((message) => (
                  <MessageScrollerItem
                    key={message.id}
                    messageId={String(message.id)}
                    scrollAnchor={message.from === "me"}
                  >
                    <Message align={message.from === "me" ? "end" : "start"}>
                      <MessageContent>
                        <Bubble
                          variant={message.from === "me" ? "default" : "muted"}
                        >
                          <BubbleContent>{message.text}</BubbleContent>
                        </Bubble>
                        <MessageFooter className="tabular-nums">
                          {message.time}
                        </MessageFooter>
                      </MessageContent>
                    </Message>
                  </MessageScrollerItem>
                ))}
              </MessageScrollerContent>
            </MessageScrollerViewport>
            <MessageScrollerButton />
          </MessageScroller>

          <form
            onSubmit={send}
            className="flex items-center gap-2 border-t border-border p-3"
          >
            <Input
              value={draft}
              onChange={(e) => setDraft(e.target.value)}
              placeholder="Type a message..."
              aria-label="Message"
            />
            <Button
              type="submit"
              size="icon"
              disabled={!draft.trim()}
              aria-label="Send message"
            >
              <IconPlaceholder
                lucide="Send"
                tabler="IconSend"
                hugeicons="SentIcon"
                phosphor="PaperPlane"
                remixicon="RiSendPlane2Fill"
                className="size-4"
                aria-hidden="true"
              />
            </Button>
          </form>
        </div>
      </MessageScrollerProvider>
    </section>
  )
}
```
