Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Orchestrating shell that wires chat-thread, chat-input, participant data, and model selection into a composed chat interface.
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.
| 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 |
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>
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>
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>
When chat-input:send fires:
chat-message[data-role="user"] and appends to threadchat-message[data-role="agent"][data-status="typing"] with animated dotschat-input during fetchendpointdata-statuschat-input and restores focusThe typing indicator and the final response share the same DOM node — no element swap, no flicker.
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]
| 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 modelchat.model = 'claude-haiku-4-5'; // Set participants dynamicallychat.participants = new Map([ ['user', { name: 'You', role: 'user' }], ['bot', { name: 'Bot', role: 'agent' }],]); // Append a messagechat.appendMessage('agent', '<p>Hello from JS!</p>', 'bot'); // Clear the threadchat.clearThread();
| 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. |
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.
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.
| 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 |
<chat-thread> — Scrollable message container<chat-message> — Individual message turns<chat-bubble> — Content bubble wrapper<chat-input> — Form-associated input