Chat
The whole chat in one tag — a transcript that scrolls, the question panel and a composer — composed for you, or from your own children.
-
<aparte-chat>
<!-- Empty, the tag composes itself: a transcript, the question panel and a composer.
center-empty keeps the composer centred until the first message. This one is answered
by a scripted model: ask for a haiku, a table, some code or the weather. -->
<aparte-chat center-empty placeholder="Ask for a haiku, a table or some code…" style="height: 26rem"></aparte-chat><aparte-chat> lays its children out as a column: the transcript takes the space left and
scrolls, the composer keeps its own height below it. Left empty, it composes itself — a
transcript, the panel where questions and approvals appear, and a composer with a field and a
send button. Put an <aparte-chat-viewport> inside and it uses your children as they are.
The tag draws the chat; something has to answer it. AparteClient listens for the messages it
sends and streams the replies back (see Getting started).
Variants
Section titled “Variants”With a conversation
Section titled “With a conversation”<aparte-chat style="height: 24rem"></aparte-chat>
<script>
// A conversation restored from your store: the viewport renders each message.
const chat = document.querySelector('aparte-chat');
for (const message of [
{ id: 'u1', role: 'user', content: 'Which transport should I use?', timestamp: Date.parse('2026-10-15T09:41:00') },
{ id: 'a1', role: 'assistant', content: 'Use **AparteDirectTransport** while you prototype: the browser talks to the model. Switch to **AparteBackendTransport** before you ship, so the key stays on your server.', timestamp: Date.parse('2026-10-15T09:41:04') },
{ id: 'u2', role: 'user', content: 'Does the component change between the two?', timestamp: Date.parse('2026-10-15T09:42:10') },
{ id: 'a2', role: 'assistant', content: 'No. The transport is configuration; the chat, its streaming and its tool calls stay the same.', timestamp: Date.parse('2026-10-15T09:42:13') },
]) chat.viewport.appendMessage(message);
</script>chat.viewport is the transcript. Restoring a conversation is appending its messages; the
client does the same during a turn.
The composer floating over the transcript
Section titled “The composer floating over the transcript”<!-- overlay-composer: the transcript scrolls under the composer, which floats at the
bottom, so the scrollbar runs the whole height of the column. -->
<aparte-chat overlay-composer style="height: 24rem"></aparte-chat>
<script>
const chat = document.querySelector('aparte-chat');
for (const message of [
{ id: 'u1', role: 'user', content: 'Which transport should I use?', timestamp: Date.parse('2026-10-15T09:41:00') },
{ id: 'a1', role: 'assistant', content: 'Use **AparteDirectTransport** while you prototype: the browser talks to the model. Switch to **AparteBackendTransport** before you ship, so the key stays on your server.', timestamp: Date.parse('2026-10-15T09:41:04') },
{ id: 'u2', role: 'user', content: 'Does the component change between the two?', timestamp: Date.parse('2026-10-15T09:42:10') },
{ id: 'a2', role: 'assistant', content: 'No. The transport is configuration; the chat, its streaming and its tool calls stay the same.', timestamp: Date.parse('2026-10-15T09:42:13') },
]) chat.viewport.appendMessage(message);
</script>overlay-composer lets the transcript scroll under the composer, which floats at the bottom:
the layout of ChatGPT, where the scrollbar runs the whole height of the column.
Composed by hand
Section titled “Composed by hand”<!-- With a viewport inside, the tag uses your children as they are, in DOM order: here a
header row, the transcript, and a composer with a toolbar. -->
<aparte-chat style="height: 26rem">
<header class="aparte-app-header">
<span class="aparte-app-header__title">Release assistant</span>
<span class="aparte-badge aparte-badge--soft aparte-badge--sm">beta</span>
</header>
<aparte-chat-viewport></aparte-chat-viewport>
<aparte-composer placeholder="Ask about the release…">
<div class="aparte-composer-shell">
<div class="aparte-composer-row">
<aparte-composer-input></aparte-composer-input>
<aparte-composer-send></aparte-composer-send>
</div>
</div>
</aparte-composer>
</aparte-chat>Anything you add is another row of the column, in DOM order: a header, a banner above the
composer. center-empty keeps the composer centred as a welcome screen until the first message
lands, then slides it down; attachments adds the file picker to the composed composer.
<aparte-chat disabled=""></aparte-chat><aparte-chat disabled=""></aparte-chat>The aparte-* tags are typed JSX intrinsics as soon as you import from @aparte/react. A presence attribute takes '', never true — React stringifies it, and disabled={false} would render disabled="false", which hasAttribute reads as on.
<template>
<aparte-chat disabled=""></aparte-chat>
</template>Declared through Vue’s GlobalComponents, so vue-tsc checks the tag in any template. A presence attribute takes '' to set and null to remove, never false.
<aparte-chat disabled=""></aparte-chat>Declared through SvelteHTMLElements, so svelte-check covers the attributes and the on: handlers. A presence attribute takes '', never false.
import { AparteChatComponent } from '@aparte/angular';<aparte-chat [disabled]="true"></aparte-chat>A standalone component whose selector IS the tag, so the real element sits in the template — @if, @for and content projection all reach it — and no CUSTOM_ELEMENTS_SCHEMA is needed.
Installation and the framework-specific traps: React · Vue · Svelte · Angular.
<aparte-chat> · class AparteChat
Attributes
| Attribute | Description |
|---|---|
placeholder | Placeholder for the composer input (default composition) |
disabled | Disables the composer |
submit-on-enter | Forwarded to the composer by value: submit-on-enter="false" makes Enter break the line and Shift+Enter send (the bare attribute, or none, keeps the default — Enter sends). The four wrappers expose the same switch as submitOnEnter. |
attachments | Add the file picker + chips strip to the default composition (opt-in: the host must consume the files — an AparteClient does, a hand-rolled loop must read event.detail.files) |
center-empty | Center the composer as a welcome state until the first message, then slide to the normal layout |
data-empty | Reflected BY the element while its viewport holds no <aparte-chat-bubble> and is not loading — set again when the conversation is cleared. With or without center-empty: the stylesheet centers the composer through aparte-chat[center-empty][data-empty], and an app styles or scripts its own empty state against aparte-chat[data-empty]. Read-only. |
overlay-composer | The ChatGPT anatomy, opt-in: the transcript’s scroll surface spans the whole column and the composer (with the rest of the bottom stack) floats over it, so the scrollbar runs edge to edge instead of stopping at the composer’s top. The viewport measures the stack and publishes --aparte-bottom-inset; content, spacer and the scroll button clear it. Read when the viewport wires its observers — set it in the initial markup. Also honoured on a wrapper’s [data-aparte-chat] root. |
framework-managed | The wrapper’s explicit hands-off signal: set it and this element composes none of its own children, because the framework owns them. Read once at connect (it is not observed), so it has to be in the initial markup. Angular’s wrapper sets it on this element — its component selector IS aparte-chat; React/Vue/Svelte render a [data-aparte-chat] div and never create this element at all. |
Properties
| Property | Type | Description |
|---|---|---|
viewport (readonly) | AparteChatViewport | null | The message viewport (yours or the default), or null before connect. |
composer (readonly) | AparteComposer | null | The composer (yours or the default), or null before connect. |
CSS variables
| Variable | Default | Description |
|---|---|---|
--aparte-chat-bottom-gap | var(--aparte-space-8) | Space below the composer, as padding-block-end on the shell (the same rule covers a wrapper’s [data-aparte-chat] root). The gap belongs to this element because padding applied from outside would also shrink the scroll area, stopping the transcript short of the edge instead of scrolling to it. |
Accessibility
Section titled “Accessibility”- The transcript is a named, focusable
role="log", so a screen reader reaches the conversation with the keyboard and hears new messages. - Everything inside keeps its own semantics: see Conversation, Message and Composer.
Related
Section titled “Related”- Composer — the part of the chat you type in.
- Conversation — the transcript on its own.
- Layouts — the chat in a full page, beside a preview, or under a floating composer.