Skip to content

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>
On this page
Open

<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.

html
<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>
Open

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.

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.

html
<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.

html
<aparte-launcher open=""></aparte-launcher>
js
el.addEventListener('aparte-launcher-toggle', (e) => use(e.detail));

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.

ts
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();

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.

html
<div id="help" defer-hydration>
  <aparte-launcher label="Help">
    <aparte-chat></aparte-chat>
  </aparte-launcher>
</div>
ts
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.

VariableWhat it sets
--aparte-launcher-widthThe open card’s width (23.75rem), never wider than the window.
--aparte-launcher-heightThe open card’s height (37.5rem), shortened to fit a short window.
--aparte-launcher-offsetHow far the button and the card sit from the window’s edges (1.25rem).
--aparte-launcher-gapThe space between the open card and the button.
--aparte-launcher-radiusThe 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.

ts
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

AttributeDescription
openThe chat is open. Reflected; set it to start open.
labelText 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.
breakpointThe 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-sheetReflected BY the element while the window is narrower than breakpoint (30rem unless you set it). Read-only.

Properties

PropertyTypeDescription
openbooleanThe chat is open.
sheet (readonly)booleanTrue while the window is under the breakpoint, where the open chat fills it.

Methods

MethodDescription
show(): voidOpen the chat.
close(): voidClose the chat.
toggle(): voidOpen the chat when it is closed, close it when it is open.

Events

EventTypeDescription
aparte-launcher-toggleCustomEvent<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

VariableDefaultDescription
--aparte-launcher-width23.75remWidth of the open card.
--aparte-launcher-height37.5remHeight of the open card, shortened to fit a short window.
--aparte-launcher-offset1.25remDistance from the window’s edges to the button and the card.
--aparte-launcher-gapvar(--aparte-space-5)Space between the open card and the button.
--aparte-launcher-radiusvar(--aparte-radius-xl)Corner radius of the open card.
  • The button’s aria-expanded follows 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() and open set 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 with show() 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 own display — 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:

    ts
    const 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;
    });
  • 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.