Skip to main content
Urbicon UI

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.

Assistant online

What does the Chat shell actually do?

Copy

A pinned header, a scrollable body, and a pinned composer. Only the body scrolls, so the page around it stays still.

Copy
<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

The header and composer stay pinned while the ChatMessageList body scrolls. You hold the messages array in $state and append to it as answers stream in.
<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

Put the shell in a SplitPane's start pane and a live artifact (preview, document, canvas) in the end pane. Each pane scrolls independently.
<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 props
9 props
Prop
Type
Default
Description

05 Types

Local type definitions used by this component.

3 types
Name
Kind
Category
Used by
Description

06 Installation

Import

import { Chat, ChatMessageList, PromptInput } from '@urbicon-ui/blocks';