Chatexperimental
A full-height layout shell for a chat: a pinned header, a scrollable message body, and a pinned composer. It holds no state of its own — the messages array lives in your component.
Playground
Chat stacks an optional header, a ChatMessageList body, and a PromptInput composer. The live playground shows the full streaming stack.
What does the Chat shell actually do?
A pinned header, a scrollable body, and a pinned composer. Only the body scrolls, so the page around it stays still.
<script lang="ts">
import { Chat, ChatMessageList } from '@urbicon-ui/blocks';
let messages = $state([
{
id: 'chat-shell-1',
role: 'user',
parts: [{ type: 'text', text: 'What does the Chat shell actually do?' }],
createdAt: new Date('2026-01-01T10:00:00.000Z'),
status: 'complete'
},
{
id: 'chat-shell-2',
role: 'assistant',
parts: [
{ type: 'text', text: 'A pinned **header**, a scrollable **body**, and a pinned **composer**. Only the body scrolls, so the page around it stays still.' }
],
createdAt: new Date('2026-01-01T10:00:05.000Z'),
status: 'complete'
}
]);
</script>
<Chat><ChatMessageList {messages} listLabel="Demo conversation" /></Chat>01 Examples
Full conversation shell
<script lang="ts">
import { Chat, ChatMessageList, PromptInput, type ChatMessageData } from '@urbicon-ui/blocks';
let messages = $state<ChatMessageData[]>([]);
let busy = $state(false);
async function handleSubmit({ text }: { text: string }) {
messages = [
...messages,
{ id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text }], status: 'complete' }
];
// …call your transport, append an assistant message, then stream
// its text part in as chunks arrive (see the live playground).
}
</script>
<!-- Chat fills its parent — give that parent a height. -->
<div class="h-[40rem]">
<Chat>
{#snippet header()}
<div class="px-4 py-2.5 text-sm font-medium text-text-primary">Support copilot</div>
{/snippet}
<ChatMessageList {messages} />
{#snippet composer()}
<div class="p-3">
<PromptInput {busy} placeholder="Ask anything…" onSubmit={handleSubmit} />
</div>
{/snippet}
</Chat>
</div>Chat beside an artifact panel
<script lang="ts">
import { SplitPane, Chat, ChatMessageList, PromptInput } from '@urbicon-ui/blocks';
let split = $state(0.55);
// messages / handleSubmit as in the composition example above
</script>
<div class="h-[40rem]">
<SplitPane bind:ratio={split} min="30%" max="70%">
{#snippet start()}
<Chat>
{#snippet header()}
<div class="px-4 py-2.5 text-sm font-medium text-text-primary">Assistant</div>
{/snippet}
<ChatMessageList {messages} />
{#snippet composer()}
<div class="p-3"><PromptInput onSubmit={handleSubmit} /></div>
{/snippet}
</Chat>
{/snippet}
{#snippet end()}
<div class="h-full overflow-auto bg-surface-elevated p-6">
<h2 class="text-sm font-semibold text-text-primary">Preview</h2>
<!-- rendered artifact… -->
</div>
{/snippet}
</SplitPane>
</div>02 Anatomy
Chat is three stacked regions:
header— an optional pinned bar (border-b). Put a title, model picker, or connection badge here. Never scrolls.children— the scrollable body. This is the one region that scrolls; drop a ChatMessageList here.composer— an optional pinned footer (border-t) — e.g. a PromptInput. Never scrolls.
Chat fills its parent's height, so give the parent a concrete height (or place
it in a flex/grid track that provides one). Inside, the body carries min-h-0 so it can scroll instead of pushing the shell taller.
Chat keeps no state of its own; the messages array lives in your component.
03 Accessibility
Structure, not a landmark
Chat renders plain <div> regions. It adds no role="banner", <main>, or <form> semantics, because those belong to the
page around it. If the conversation is a standalone view, give the root an aria-label or wrap it in a labelled <section>.
Meaning comes from children
The accessible semantics come from what you place inside. ChatMessageList provides the scrollable role="log" region and a screen-reader status channel; PromptInput provides the labelled textarea and send/stop
buttons. Chat itself only stacks them in order.
Scroll ownership
Because only the body scrolls, Page Up / Page Down act on the focusable list region rather than the page, and the header and composer stay in view.
04 API Reference
9 propsProp | Type | Default | Description | |
|---|---|---|---|---|
children | Snippet | — | The scrollable conversation area — typically a ChatMessageList. | |
class | string | — | Extra classes merged onto the root element. | |
composer | Snippet | — | Optional pinned composer rendered below the conversation (border-t) — e.g. PromptInput. | |
header | Snippet | — | Optional pinned header rendered above the conversation (border-b). | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ Chat: {...} }}>.
Prefer this over class overrides for reusable custom looks. | |
slotClasses | Partial<Record<ChatSlots, string>> | — | Per-slot class overrides. Slots: root | header | body | composer | |
unstyled | boolean | — | Remove all default tv classes. | |
...ChatVariants variant | VariantProps | — | Styling variants from ChatVariants | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children' | 'class') |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
ChatProps | interface | props | 0 | — | |
ChatVariants | type | variant | 1 | — | |
ChatSlots | type | variant | 0 | Slot names derived from the tv() config — single source of truth for slotClasses. |
06 Installation
Import
import { Chat, ChatMessageList, PromptInput } from '@urbicon-ui/blocks';