Skip to content

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>
Width Open in a tab
The real element, in its own document so this site's CSS cannot reach it. It renders the same example printed below — every framework mounts this same element, so the rendering is what you get in all five; the code is what changes.
<!-- 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>
AttributeDescription
placeholderPlaceholder for the composer input (default composition)
disabledDisables the composer
submit-on-enterForwarded 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-emptyCenter the composer as a welcome state until the first message, then slide to the normal layout
attachmentsAdd 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-emptyReflected 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-composerThe 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-managedThe 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.
PropertyTypeDescription
viewport (readonly)AparteChatViewport | nullThe message viewport (yours or the default), or null before connect.
composer (readonly)AparteComposer | nullThe composer (yours or the default), or null before connect.

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.

VariableDefaultDescription
--aparte-chat-bottom-gapvar(--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.

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>

Installation and the framework-specific traps: React · Vue · Svelte · Angular.