Skip to content

Chat status

<aparte-chat-status>

A standalone status line — a light-DOM indicator the APP owns. Nothing in core turns it on: the framework host only ever flips it back OFF (on the first streamed token), and the four wrappers render one inside the viewport driven by their own isTyping prop.

It dispatches nothing, deliberately: it reports, it does not ask.

Use it for a state only the app knows about — “Searching the docs…”, “Uploading…”, a queue position. It is NOT the indicator for the gap between a send and the first token: that one is built into the bubble (.aparte-waiting, shown while streaming is set on a non-user bubble that has nothing to display yet). Turning this element on for that gap is how a page ends up showing two indicators for one wait.

It does not project children: _render() writes the subtree — a .aparte-status-container row holding an empty avatar div and an .aparte-body that wraps .aparte-status-content — so markup authored between the tags does not survive. That avatar div never gets contents here, and .aparte-avatar:empty is display: none, so it is not a spacer: the line sits flush with the row padding rather than indented under an assistant bubble’s text column.

The seam for custom contents is setStatusRenderer, scoped or global: the container keeps owning show/hide (data-visible), the accessible name (aria-label) and its .aparte-message row metrics whatever the renderer returns, but the pulsing dot and the text node belong to the default path only.

A text attribute renders the visible label and feeds the accessible name; with no text the line is dots-only and the name falls back to the literal Typing — this element does not read the locale. The container is a polite live region, and a live region announces its CONTENT, not its aria-label: so the word also goes INTO the region, in the visible span when text is set and in a screen-reader-only span when it is not. Exactly one of the two is ever populated — both would be read twice. That fallback word is driven by visible, not written once at render: a live region is announced when its content CHANGES while it is exposed, and every wrapper mounts this element hidden and flips the attribute — so a word written at render time is a word the region already held when it appeared, the reveal-from-display: none path, which is the one screen readers are documented not to announce reliably. Hiding happens twice over: the host element is display: none without [visible], and data-visible drives the fade/translate on the container.

The config is resolved live rather than cached, so a setStatusRenderer call that lands after this element has already upgraded still reaches it: the element re-renders on aparte-config-change, filtered to its own config.

The two borrowed row variables below have one scope caveat: inside a viewport narrower than 520px core REASSIGNS --aparte-message-padding-block and --aparte-message-padding-inline on .aparte-message itself, so a declaration on this host element loses to them there.

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.
<!-- The app owns this indicator: core turns it on for nobody, which is also why it
is the wrong tool for the wait before the first token — the bubble's built-in
waiting state already covers that one. -->
<aparte-chat-status visible text="Searching the docs…"></aparte-chat-status>
AttributeDescription
visibleShows or hides the indicator.
textThe line to show. Absent, the line is dots-only and the accessible name falls back to the literal Typing (not the locale’s string).
MethodDescription
show(): voidShow the typing indicator
hide(): voidHide the typing indicator
toggle(): voidToggle visibility
isVisible(): booleanCheck if visible

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-status-colorvar(--aparte-text-muted)Colour of the label text and of the pulsing dot in the default line.
--aparte-status-font-sizevar(--aparte-font-size-md)Size of the visible label (italic by default) in the default line.
--aparte-status-dot-size6pxDiameter of the single pulsing dot in the default line.
--aparte-message-padding-blockvar(--aparte-space-8)Vertical padding of the row, read because the container also carries .aparte-message — the status line borrows a bubble’s row metrics so it lines up with the transcript.
--aparte-message-padding-inlinevar(--aparte-space-6)Horizontal padding of the row, same reason.
--aparte-message-max-width800pxWidth cap of that same row.

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-status visible=""></aparte-chat-status>

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