chat-window

Orchestrating shell that wires chat-thread, chat-input, participant data, and model selection into a composed chat interface.

Overview

The orchestrating shell for chat UIs. chat-window wires together <chat-thread>, <chat-input>, participant data, and an optional model selector into a complete chat interface.

Light DOM, no Shadow DOM. Uses CSS grid with grid-template-rows: auto 1fr auto for header/thread/input layout. The consumer sizes the container; chat-window fills it.

Attributes

Attribute Type Default Description
endpoint URL - API endpoint for chat requests
model string - Active model; synced with [data-model-select]
empty-message string Send a message to start. Text shown when thread is empty

AI Chat with Model Selector

The full compose: header with title and model selector, empty thread, and input area. The [data-model-select] dropdown syncs bidirectionally with model.

<chat-window endpoint="/api/ai" model="claude-sonnet-4-6"> <script type="application/json" data-participants> { "user": { "name": "You", "role": "user" }, "assistant": { "name": "Assistant", "role": "agent" } } </script> <header> <h3>AI Assistant</h3> <select data-model-select aria-label="Select model"> <option value="claude-sonnet-4-6">Sonnet 4.6</option> <option value="claude-opus-4-6">Opus 4.6</option> <option value="claude-haiku-4-5">Haiku 4.5</option> </select> </header> <chat-thread role="log" aria-label="AI conversation" aria-live="polite"> </chat-thread> <chat-input name="message"> <textarea data-grow rows="1" data-max-rows="8" placeholder="Ask anything..."></textarea> <footer> <button type="submit" class="small" data-send aria-label="Send"> <icon-wc name="send"></icon-wc> </button> </footer> </chat-input> </chat-window>

Support Chat with SSR Messages

Server-rendered messages hydrate on connect. chat-window resolves data-from IDs against the participant map and writes data-from-label for CSS rendering.

<chat-window endpoint="/api/support"> <script type="application/json" data-participants> { "user": { "name": "You", "role": "user" }, "sarah": { "name": "Sarah Chen", "role": "agent" } } </script> <header> <h3>Support Chat</h3> </header> <chat-thread role="log" aria-label="Support conversation" aria-live="polite"> <chat-message data-role="agent" data-from="sarah"> <chat-bubble><p>Welcome! How can I help?</p></chat-bubble> </chat-message> </chat-thread> <chat-input name="message"> <textarea data-grow rows="1" placeholder="Type a message..."></textarea> <footer> <button type="submit" class="small" data-send aria-label="Send"> <icon-wc name="send"></icon-wc> </button> </footer> </chat-input> </chat-window>

Participant Data

Declared via a <script type="application/json" data-participants> block inside chat-window, or set programmatically via the participants setter. Keys are participant IDs used in data-from attributes.

<script type="application/json" data-participants> { "user": { "name": "You", "role": "user" }, "sarah": { "name": "Sarah Chen", "role": "agent", "avatar": "/avatars/sarah.jpg" }, "assistant": { "name": "Assistant", "role": "agent", "model": "claude-sonnet-4-6" } } </script>

Orchestration Flow

When chat-input:send fires:

  1. Builds chat-message[data-role="user"] and appends to thread
  2. Builds chat-message[data-role="agent"][data-status="typing"] with animated dots
  3. Disables chat-input during fetch
  4. Awaits response from endpoint
  5. Populates the typing message with real content; removes data-status
  6. Re-enables chat-input and restores focus

The typing indicator and the final response share the same DOM node — no element swap, no flicker.

DOM Structure

chat-window ├── script[data-participants] (optional, hidden) ├── header │ ├── h3 (title) │ └── select[data-model-select] (optional) ├── chat-thread[role="log"] │ └── chat-message elements └── chat-input ├── textarea[data-grow] └── footer > button[data-send]

Public API

Member Type Description
model string (get/set) Current model; syncs with select
participants Map (get/set) Participant registry
clearThread() method Remove all messages
appendMessage(role, html, from?) method Programmatic message append
const chat = document.querySelector('chat-window'); // Change model chat.model = 'claude-haiku-4-5'; // Set participants dynamically chat.participants = new Map([ ['user', { name: 'You', role: 'user' }], ['bot', { name: 'Bot', role: 'agent' }], ]); // Append a message chat.appendMessage('agent', '<p>Hello from JS!</p>', 'bot'); // Clear the thread chat.clearThread();

Events

Event Detail Description
chat-window:send { message, typingElement } Dispatched when a message is sent and no endpoint is set. Use this for custom transport (WebSocket, worker, etc.). Populate the typing element's chat-bubble with the response.
chat-window:error { error, status } Dispatched when the built-in fetch transport fails.
chat-window:model-change { model } Dispatched when the model selector value changes.

Participant Identity

The data-participants JSON map uses arbitrary string IDs as keys. User messages are created with data-from="user", and the agent is resolved as the first participant whose role is "agent". Match these conventions in your participant map.

Model Property

The model attribute is synced with the <select data-model-select> at connect time. After connect, use the .model property setter for updates — attribute mutations are not observed.

CSS Variables

Variable Default Description
--chat-window-header-bg var(--color-surface) Header background
--chat-window-header-border var(--border-width-thin) solid var(--color-border) Header bottom border

Related