Chat
Chat shell, scrolling body, and message composer primitives.
Quick Preview
Adaeze Okafor
Online
Composition
Chat is a column of a fixed header, a scrolling body, and a fixed footer. The
header and footer sit outside the scroll region, so only ChatBody moves.
Chat (give it a bounded height)
├── ChatHeader
│ ├── ChatHeaderAction (back)
│ ├── ChatHeaderIdentity › ChatHeaderText › title / description
│ └── ChatHeaderActions › ChatHeaderAction…
├── ChatBody (the only scrolling region)
│ ├── ChatDateSection (bounds one dated message group)
│ │ ├── ChatDateDivider (sticky only inside its section)
│ │ └── MessageGroup › Message…
│ ├── ChatBodyBar (optional) (sticky tabs / filter row)
└── ChatFooter
└── ChatInput
├── ChatInputAttach
├── ChatInputField › ChatInputAction (optional)
└── ChatInputSendChatBody keeps the newest message in view while the reader is at the bottom,
hands control over the moment they scroll up, and holds the reading position when
older messages load in above. Give Chat a height (h-125, h-full, h-dvh)
— the body can only scroll inside a bounded parent.
Usage
<Chat aria-label="Conversation with Tunde Bakare" className="h-125">
<ChatHeader>
<ChatHeaderAction aria-label="Back"><ArrowLeft /></ChatHeaderAction>
<ChatHeaderIdentity>
<Avatar><AvatarFallback>AO</AvatarFallback></Avatar>
<ChatHeaderText>
<ChatHeaderTitle>Adaeze Okafor</ChatHeaderTitle>
<ChatHeaderDescription>Online</ChatHeaderDescription>
</ChatHeaderText>
</ChatHeaderIdentity>
<ChatHeaderActions>
<ChatHeaderAction aria-label="Start video call"><Video /></ChatHeaderAction>
<ChatHeaderAction aria-label="Start voice call"><Phone /></ChatHeaderAction>
</ChatHeaderActions>
</ChatHeader>
<ChatBody>
<ChatDateSection>
<ChatDateDivider dateTime="2026-07-22">Today</ChatDateDivider>
<MessageGroup>{/* …messages */}</MessageGroup>
</ChatDateSection>
</ChatBody>
<ChatFooter>
<ChatInput onSend={handleSend}>
<ChatInputAttach onAttach={handleAttach} />
<ChatInputField placeholder="Type a message" />
<ChatInputSend />
</ChatInput>
</ChatFooter>
</Chat>Examples
Full conversation
A bounded Chat with a header, a following ChatBody, and a composer. ChatInput
is a real <form>, so Enter and the send button share one submit path; send a
message and the body stays pinned to the newest bubble.
Adaeze Okafor
Online
Composer
ChatInput is composable on its own. Drop ChatInputAction buttons inside
ChatInputField for inline controls (emoji, mic), and the send button disables
itself while the field is empty.
Custom attachment menu
ChatInputAttach defaults to File / Video / Image, but the menu is data-driven
via items. Spread defaultChatInputAttachItems to add kinds without losing the
defaults. An item with accept opens a filtered file picker (onAttach fires
with the files and the item's kind); an item with onSelect runs a custom,
non-file action instead — for "Share location", "Send contact", and the like.
<ChatInputAttach
onAttach={handleAttach}
items={[
...defaultChatInputAttachItems,
{ kind: "document", label: "Document", icon: <FileText />, accept: ".pdf,.doc" },
{ kind: "location", label: "Share location", icon: <MapPin />, onSelect: shareLocation },
]}
/>Props
| Prop/API | Type | Default | Description |
|---|---|---|---|
value on ChatInput | string | - | Controlled field value. Omit for uncontrolled. |
defaultValue on ChatInput | string | "" | Initial value when uncontrolled. |
onValueChange on ChatInput | (value: string) => void | - | Fires on every keystroke. |
onSend on ChatInput | (value: string) => unknown | - | Fires on submit with the trimmed text. Return false to keep the field. |
clearOnSend on ChatInput | boolean | true | Clear an uncontrolled field after onSend accepts. |
disabled on ChatInput | boolean | false | Blocks typing, sending, and attaching. |
onAttach on ChatInputAttach | (files: File[], kind: string) => void | - | Fires with the picked files and the requested kind. |
items on ChatInputAttach | ChatInputAttachItem[] | File / Video / Image | Menu entries. Spread defaultChatInputAttachItems to extend; use accept for a file picker or onSelect for a custom action. |
align on ChatInputAction | "inline-start" | "inline-end" | "inline-start" | Which side of the field owns the action. |
useChatBody() | { isPinned, scrollToBottom } | - | Read scroll state for a "jump to bottom" affordance. Call inside ChatBody. |
className | string | - | Local layout or spacing overrides. |
| native/root props | React component props | - | Passed through to the underlying element. |
Accessibility
- Give
Chatanaria-labelnaming the conversation, and a bounded height so the body scrolls rather than the page. ChatInputis a<form>; Enter submits (with an IME-composition guard) and the send button is the same submit path.ChatInputAttachand eachChatInputActionare icon-only — provide anaria-label(the attach and send controls default one).- Preserve the header/body/footer structure so the fixed regions stay out of the scroll and assistive tech reads a stable landmark order.
- Do not communicate state with colour alone; pair status with text or icons.