The canonical composition pattern for an LLM chat surface. Built from
cluster-namespaced bespoke children — every structural part has its
own custom element. The legacy raw-HTML form (raw <header>
with data-chat-name / data-chat-status,
<section data-chat-messages>,
<empty-state-ui data-chat-empty>,
<footer> wrapping <chat-input-ui>)
was retired in v0.4.0 per
ADR-0024;
new code uses the bespoke shape exclusively per
ADR-0023.
For the full live demo + state-as-attribute documentation, see /site/components/chat-shell. This page is the pattern hub.
| Part | Role |
|---|---|
<chat-shell> |
Behavior-only host. Wires LLM streaming, markdown rendering, code-block upgrades, the proxy-url integration path. |
<chat-header> |
Top band — slots: name, status, action. |
<chat-thread> |
Streaming message list. Reflects [streaming]. |
<chat-empty> |
Empty-state hint inside <chat-thread> before the first message. |
<chat-composer> |
Input area. Wraps <chat-input-ui>. |
<chat-sidebar> |
Optional conversation-list / inspector rail (slot="leading" or slot="trailing"). |
For a multi-conversation surface, add a <chat-sidebar slot="leading"> with a <nav-ui> of past chats.
<chat-header>, <chat-thread>, <chat-composer>, <chat-sidebar>. Don't author raw <header data-chat-name> or <section data-chat-messages> — these no longer style or wire.shell.querySelector('chat-thread').hasAttribute('streaming'). Style cross-cuts via :has(); no JS coordination needed.<chat-empty> inside <chat-thread>. The thread hides the empty wrapper automatically when the first message arrives.<chat-input-ui>. Don't put <chat-input-ui> as a direct child of <chat-shell>.