Launcher
The chat that floats in a corner of the page — a button at the bottom of the window that opens the chat as a card, and as a full-window sheet on a phone.
-
<aparte-launcher>
<!-- In a page it floats over the window; here, over this frame. Press the button, then it, Escape or ✕ to close.
A support card: who answers at the top, a greeting, and three questions to start from. -->
<style>
.support { display: flex; flex-direction: column }
.support header { display: grid; padding: var(--aparte-space-3) var(--aparte-space-4); border-block-end: var(--aparte-border-width) solid var(--aparte-border) }
.support header small { color: var(--aparte-text-muted) }
.support aparte-chat { flex: 1; min-height: 0 }
</style>
<div style="height: 32rem"></div>
<aparte-launcher label="Help" breakpoint="none" style="--aparte-launcher-height: 26rem">
<section class="support">
<header><strong>Kilnhouse support</strong><small>Usually answers in a minute</small></header>
<aparte-chat id="help">
<aparte-chat-viewport>
<aparte-chat-bubble message-id="hello" data-role="assistant" name="Assistant"
content="Hi! I can track an order, change a delivery address or start a return."></aparte-chat-bubble>
</aparte-chat-viewport>
<aparte-composer placeholder="Ask about your order…">
<aparte-suggestions suggestions='["Where is my order?", "Change my address", "Start a return"]'></aparte-suggestions>
<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>
</section>
</aparte-launcher>
<script>
// The chat inside is an ordinary <aparte-chat>: here a script answers it; in a page, AparteClient.
const chat = document.getElementById('help');
let n = 0;
chat.addEventListener('aparte-send', (event) => {
// The greeting is already a message, so the starters go with the first question.
chat.querySelector('aparte-suggestions').hidden = true;
chat.viewport.appendMessage({ id: 'u' + ++n, role: 'user', content: event.detail.content, timestamp: Date.now() });
chat.viewport.appendMessage({ id: 'a' + ++n, role: 'assistant', content: 'Your order left the warehouse this morning.', timestamp: Date.now() });
});
</script><aparte-launcher> is the support widget a product site puts at the bottom of every page, as an
element. A round button floats at the bottom of the window, on the inline-end side; a click opens
the chat above it as a card, and a second click, Escape, or a control of yours closes it.
Its content is yours: an <aparte-chat>, or your own card with a header above one. The element
adds its button after them and shows them only while it is open, so a closed launcher keeps the
chat mounted — the conversation and the client stay — but out of sight, out of the tab order and
out of the accessibility tree. It stores nothing: aparte-launcher-toggle fires on every change,
for you to remember the state.
<aparte-launcher label="Help">
<aparte-chat></aparte-chat>
</aparte-launcher>
<script type="module">
import { AparteClient } from '@aparte/core';
new AparteClient().start(); // the chat inside answers like any other
</script>Variants
Section titled “Variants”As a sheet on a phone
Section titled “As a sheet on a phone”<!-- Under its breakpoint the open chat fills the window and its button moves to the top corner.
A breakpoint wider than this frame keeps it a sheet here. -->
<div style="height: 30rem"></div>
<aparte-launcher open breakpoint="200rem">
<aparte-chat>
<aparte-chat-viewport>
<aparte-chat-bubble message-id="u1" data-role="user" content="Can I change my delivery address?"></aparte-chat-bubble>
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant" content="Yes, until the parcel ships. Which address should it go to?"></aparte-chat-bubble>
</aparte-chat-viewport>
<aparte-composer placeholder="Type the new address…">
<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>
</aparte-launcher>Under breakpoint (30rem of window by default, in width or in height — a phone held sideways too — or none for a card at any size), the open chat
fills the window, Tab stays inside it, and the button moves to the top corner to close it. The
element carries data-sheet while the window is that narrow.
Your own card
Section titled “Your own card”A card with its own header and close button goes in as one child. Mark the button
data-aparte-launcher-close and it closes the launcher with no script; on a phone, the sheet then
hides the launcher’s own button, since yours is already there.
<aparte-launcher label="Help">
<section style="display: flex; flex-direction: column">
<header style="display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem">
<strong>Kilnhouse support</strong>
<button class="aparte-btn aparte-btn--icon aparte-btn--ghost" type="button"
aria-label="Close the help chat" data-aparte-launcher-close>
<aparte-icon name="close"></aparte-icon>
</button>
</header>
<aparte-chat style="flex: 1; min-height: 0"></aparte-chat>
</section>
</aparte-launcher>The launcher sizes its child; a card that stacks a header over the chat lays itself out as a
column and lets the chat take the rest, which is what the two style attributes do.
<aparte-launcher open=""></aparte-launcher>el.addEventListener('aparte-launcher-toggle', (e) => use(e.detail));<aparte-launcher open=""></aparte-launcher>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 open={false} would render open="false", which hasAttribute reads as on. Events reach you by ref, typed through the DOM.
<template>
<aparte-launcher open=""
@aparte-launcher-toggle="(e) => use(e.detail)"
></aparte-launcher>
</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-launcher open=""
on:aparte-launcher-toggle={(e) => use(e.detail)}
></aparte-launcher>Declared through SvelteHTMLElements, so svelte-check covers the attributes and the on: handlers. A presence attribute takes '', never false.
import { AparteLauncherDirective } from '@aparte/angular';<aparte-launcher [open]="true"
(launcherToggle)="use($event)"></aparte-launcher>A standalone directive 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.
label puts text beside the bubble, and the button reads as a pill; without it the button is
round and named by the locale (launcherOpen and launcherClose). The locale and the icons are
those of the chat inside when it carries its own config — the config a wrapper takes, or
attachConfig(chat, config) — and the page’s otherwise, so a French chat gets a French button
without a global setting. open opens it from the markup or from a script, and show(),
close() and toggle() do the same.
const launcher = document.querySelector('aparte-launcher');
launcher?.addEventListener('aparte-launcher-toggle', (event) => {
sessionStorage.setItem('help-open', String(event.detail.open));
});
if (sessionStorage.getItem('help-open') === 'true') launcher?.show();Server rendering
Section titled “Server rendering”The launcher draws its button when it connects. On a server-rendered page that is as soon as
@aparte/core loads — before your framework hydrates — and hydration then removes the button
it did not render. The chat inside already holds itself back until its framework has the page,
but that does not hold the launcher around it. Hold the launcher too: an element of yours that
carries defer-hydration in the server’s HTML, and the release once the framework has
hydrated.
<div id="help" defer-hydration>
<aparte-launcher label="Help">
<aparte-chat></aparte-chat>
</aparte-launcher>
</div>import { releaseDeferredHydration } from '@aparte/core';
// From your framework's mounted hook (useEffect, onMounted, onMount, ngAfterViewInit),
// once the page has hydrated:
releaseDeferredHydration(document.getElementById('help'));Render defer-hydration on that element in the server render and in the hydration render
alike, so the framework finds the markup it sent. Until the release the launcher draws nothing;
once it is released it draws its button, and the chat inside wakes with it.
Hydration says how the release works.
If something removes the button anyway — a page that skips the release — the launcher puts it back, so the chat stays reachable; the framework still reports the mismatch, which holding the launcher avoids.
Customize
Section titled “Customize”| Variable | What it sets |
|---|---|
--aparte-launcher-width | The open card’s width (23.75rem), never wider than the window. |
--aparte-launcher-height | The open card’s height (37.5rem), shortened to fit a short window. |
--aparte-launcher-offset | How far the button and the card sit from the window’s edges (1.25rem). |
--aparte-launcher-gap | The space between the open card and the button. |
--aparte-launcher-radius | The open card’s corners. |
The button wears the button recipe — solid, primary, large — and the
launcher sits at --aparte-z-dropdown, above the page and below what you raise over it.
Its two icons have names of their own, so you can redraw the button without redrawing every
close button in the chat: launcherOpen (shown while it is closed) and launcherClose. Without
them it draws messageSquare and close.
import { aparteGlobalConfig } from '@aparte/core';
aparteGlobalConfig.registerIcons({
launcherOpen: () => '<svg class="aparte-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M5 3h14a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-7l-5 4v-4H5a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3z"/></svg>',
launcherClose: () => '<svg class="aparte-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>',
});<aparte-launcher> · class AparteLauncher
Attributes
| Attribute | Description |
|---|---|
open | The chat is open. Reflected; set it to start open. |
label | Text shown beside the bubble on the closed button, which then reads as a pill. Without it the button is round and named by the locale. |
breakpoint | The window width under which the open chat fills the window: a length, or none for a card at any size. Unset, a window under 30rem wide or 30rem high. |
data-sheet | Reflected BY the element while the window is narrower than breakpoint (30rem unless you set it). Read-only. |
Properties
| Property | Type | Description |
|---|---|---|
open | boolean | The chat is open. |
sheet (readonly) | boolean | True while the window is under the breakpoint, where the open chat fills it. |
Methods
| Method | Description |
|---|---|
show(): void | Open the chat. |
close(): void | Close the chat. |
toggle(): void | Open the chat when it is closed, close it when it is open. |
Events
| Event | Type | Description |
|---|---|---|
aparte-launcher-toggle | CustomEvent<AparteLauncherToggleDetail> | The chat opened or closed, by the button, by Escape, by a close control or by open being set — or, while it is open, turned into a sheet or back as the window crossed the breakpoint. Bubbles. |
CSS variables
| Variable | Default | Description |
|---|---|---|
--aparte-launcher-width | 23.75rem | Width of the open card. |
--aparte-launcher-height | 37.5rem | Height of the open card, shortened to fit a short window. |
--aparte-launcher-offset | 1.25rem | Distance from the window’s edges to the button and the card. |
--aparte-launcher-gap | var(--aparte-space-5) | Space between the open card and the button. |
--aparte-launcher-radius | var(--aparte-radius-xl) | Corner radius of the open card. |
Accessibility
Section titled “Accessibility”-
The button’s
aria-expandedfollows the state; it is named by its label, or by the locale when it shows only the bubble or the ✕. -
Opening with the button hands the focus to the composer, or to the first control of your card; closing gives it back to the button when the focus was inside.
show()andopenset from a script move no focus, so the snippet above that restores the state on page load does not steal it. A control of yours that opens the chat withshow()and wants the composer ready focuses it itself, right after. -
Escape closes it from anywhere inside, unless something there has the key first: a control that answered it (a menu closing, the context gauge’s panel), an input method that is composing text, or a modal
<dialog>open in your card, which closes itself on it. The next Escape closes the launcher. -
A closed launcher’s content is
display: none, and stays so when your card sets its owndisplay— inline, or by a rule of any weight: nothing invisible keeps a tab stop. -
The sheet keeps Tab inside it while it covers the page; the card leaves the page reachable.
-
The page behind the sheet is still in the accessibility tree, since the launcher does not own it. Take your page’s container out while the sheet covers it — the event fires again when the window crosses the breakpoint with the chat open, so no media query of yours has to match
breakpoint:tsconst launcher = document.querySelector('aparte-launcher'); const page = document.querySelector('main'); // your page, never an ancestor of the launcher launcher?.addEventListener('aparte-launcher-toggle', (event) => { if (page) page.inert = event.detail.open && event.detail.sheet; });
Related
Section titled “Related”- Sidebar — the column beside a full-page chat.
- Split — a chat beside a pane of your own.
- Support widget — a whole page built around a floating chat.