Chat
<aparte-chat>
AparteChat - The Shell
The container element for a chat. It lays out its Light DOM children as a flex
column: an <aparte-chat-viewport> takes the space left over (flex: 1 1 auto)
and scrolls, an <aparte-composer> keeps its own height below it. Light DOM on
purpose, so the page’s own global CSS reaches inside.
The presence of an <aparte-chat-viewport> child at connect is the exact test for
“the author composed this”. Find one and the children are used as given — this
element moves none of them, so anything else you drop in (a header, a banner above
the composer) is simply another row of that column, in DOM order. Find none and
innerHTML is OVERWRITTEN with a default composition — a viewport, an
<aparte-elicitation> presenter, and a composer shell holding an input and a send
button, plus the two attachment primitives when attachments is set — so children
written without a viewport anywhere inside them are destroyed, that header included.
The test is a DESCENDANT query, so a viewport nested in a wrapper of your own still
counts — compose it yourself with the viewport somewhere in the tree, or leave the tag
empty. Angular’s wrapper sets framework-managed instead of relying on that test,
because its children do not exist yet when this element upgrades; React, Vue and
Svelte never create this element at all, so the question does not arise for them.
Being a component (not a bare <div>), it also owns behaviour a wrapper div
can’t: with center-empty, it watches its own viewport and keeps the composer
centered as a welcome state until the first <aparte-chat-bubble> lands, then
slides to the normal layout — no external JavaScript (data-empty, in the attribute
list below, is that watcher’s output). The watcher needs a viewport somewhere
inside, and hand-written markup always has one because composing the default injects
it — so the only path where no watcher starts and data-empty is never set is
framework-managed, where the framework owns the subtree anyway. The stylesheet
centers through
aparte-chat[center-empty][data-empty] and its DIRECT viewport child, so a
framework-managed host that nests the viewport inside a container of its own gets
nothing from the attribute — the wrappers ship their own centered layout.
It is also one of the anchors where core re-declares its derived CSS layer, so
overriding a master — --aparte-primary, a surface, a text colour — on a single
<aparte-chat> re-derives the values computed from it for that instance rather
than moving one button. That is per-instance theming. The literal palette is
deliberately not re-declared here, so a chat nested in a dark wrapper stays dark.
Presentational only: it does NOT wire a transport/client. Attach an
AparteClient, or handle aparte-send yourself, as with the primitives.
Size the element via CSS (a height, or let it fill a sized parent).
Composing it yourself is the other form, and the container still lays it out and still
runs center-empty. It is written out here rather than as a second @example for a
mechanical reason: every element-own example is concatenated into ONE live frame on the
generated reference page, so a second <aparte-chat> there rendered as a second whole
chat — two empty composers with 600px of nothing between them.
<aparte-chat center-empty attachments style="height: 24rem"> <aparte-chat-viewport></aparte-chat-viewport> <aparte-composer> <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>Example
Section titled “Example”<!-- Left empty it fills in a viewport, an input and a send button. --><aparte-chat center-empty placeholder="Say something…" style="height: 24rem"></aparte-chat>
<script> // Seeded so the frame shows a real exchange rather than an empty box: this // example is RENDERED, not only read. const chat = document.querySelector('aparte-chat'); chat.viewport.appendMessage({ id: 'u1', role: 'user', content: 'What is a transport?' }); chat.viewport.appendMessage({ id: 'a1', role: 'assistant', content: 'The object that talks to the model. Swap it and the UI does not change.', });</script>Attributes
Section titled “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. |
center-empty | Center the composer as a welcome state until the first message, then slide to the normal layout |
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) |
data-empty | Reflected BY the element while center-empty is set and no <aparte-chat-bubble> has landed in its viewport; the stylesheet centers the composer through aparte-chat[center-empty][data-empty], and an app styles its welcome state against it. Never set without center-empty, and never under framework-managed (no viewport child to watch). 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
Section titled “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. |
Theming
Section titled “Theming”Override any of these on :root, on a subtree, or on one instance — custom properties inherit downward. Some are this element’s own; others are site-wide tokens that also style it, and overriding one of those at :root moves everything that reads it. The full set is in the CSS variables reference.
<aparte-chat>
Section titled “<aparte-chat>”| 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. |
In a framework
Section titled “In a framework”The element is the same object everywhere — the tag does not change. What changes is how an attribute is written and how an event reaches you.
<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.