Svelte 4/5 AI chat component, with stores — @aparte/svelte
@aparte/svelte wraps @aparte/core for Svelte 4 and 5: an ergonomic <AparteChat> component, store
factories for state and the client, and a generic <AparteUi> escape hatch.
npm install @aparte/svelte @aparte/core svelte@aparte/core and svelte are peer dependencies.
<AparteChat> + createAparteChat
Section titled “<AparteChat> + createAparteChat”The createAparteChat store factory owns the messages store and mirrors the imperative API, so you
bind the store and connect the component with bind:this:
<script lang="ts"> import { AparteChat, createAparteChat, type AparteChatImperativeApi } from '@aparte/svelte'; import '@aparte/core/styles.css';
const chat = createAparteChat(); const { messages } = chat; let comp: AparteChatImperativeApi | null = null; $: chat.connect(comp);</script>
<AparteChat bind:this={comp} messages={$messages} centerWhenEmpty onmessagesChange={(m) => chat.onMessagesChange(m)}> <p slot="empty-state">Ask me anything…</p></AparteChat>The user’s message is appended to the thread automatically on send — don’t add it yourself.
onmessageSent is optional and fires after that append, for side-effects only (scroll, analytics,
a backend call).
Every callback also exists as a component event — on:messagesChange={(e) => chat.onMessagesChange(e.detail)}
— which is the Svelte 4 spelling and still works on Svelte 5. Svelte 5 documents createEventDispatcher
as deprecated and recommends callback props, so the callbacks are the path written here; both fire for
the same occurrence, the callback with the payload itself, the event with it under event.detail.
Slots are named slots: empty-state, composer, above-composer,
toolbar (the composer’s bottom row — mode picker, model selector: see
The composer toolbar for an example; use
<svelte:fragment slot="toolbar"> to project several nodes), and
the bubble slot (<div slot="bubble" let:message>) for a fully custom bubble. Every imperative
method (streaming, branch/edit, scrollToBottom) is mirrored on the chat store and reachable via
bind:this.
The other five are onaction, onmessagesChange, onmessageAppended, ontypingChange and onconversationCreated (as events: on:action, on:messagesChange, … with the payload under event.detail) — the table with all four frameworks side by side is generated from the wrapper source: Wrapper surface.
Wiring a real model
Section titled “Wiring a real model”The wrapper is provider-agnostic. Register a provider + transport once (see
Providers) and start an AparteClient with createAparteClient — it bridges composer
sends to the model:
<script lang="ts"> import { AparteChat, createAparteChat, createAparteClient, type AparteChatImperativeApi } from '@aparte/svelte'; import { aparteGlobalConfig, AparteDirectTransport } from '@aparte/core'; import { createOpenAICompatProvider, presets } from '@aparte/provider-openai-compat';
aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider(presets.OPENROUTER)); aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));
const chat = createAparteChat(); createAparteClient(); // streams replies from the configured provider const { messages } = chat; let comp: AparteChatImperativeApi | null = null; $: chat.connect(comp);</script>
<AparteChat bind:this={comp} messages={$messages} onmessagesChange={(m) => chat.onMessagesChange(m)} />Pass a per-instance config prop to scope providers/transport to a single <AparteChat> instead of
aparteGlobalConfig.
Any aparté element: typed in the markup
Section titled “Any aparté element: typed in the markup”The aparte-* tags are declared through SvelteHTMLElements, so svelte-check covers both their
attributes and their on: handlers — no AparteUi needed:
<aparte-select searchable="" placeholder="Pick a model" on:aparte-select-change={(e) => use(e.detail.value)}> <aparte-option value="gpt-4o-mini">GPT-4o mini</aparte-option></aparte-select>On Svelte 5 in runes mode, on: on an element is the one form the compiler flags as deprecated
(event_directive_deprecated); write the event attribute instead — onaparte-select-change={(e) => …} —
which svelte-check types the same way.
Presence attributes take '' to set and null to remove, never false — Svelte stringifies what it
sets on a custom element, so searchable={false} would render searchable="false" and an element
testing hasAttribute reads that as on. The rules and the full set are on
Placing elements, typed.
Any OTHER element: <AparteUi>
Section titled “Any OTHER element: <AparteUi>”For an element aparté does not define — one of yours, or a third party’s:
<script lang="ts"> import { AparteUi } from '@aparte/svelte';</script>
<AparteUi name="my-token-counter" props={{ 'data-budget': '8000' }} onelementEvent={(e) => console.log(e.type, e.detail)}/>onelementEvent receives the element’s own CustomEvent. The component event on:elementEvent
still fires too, with that event under e.detail — the Svelte 4 spelling.
It mounts any tag name, which is what a foreign element needs and what aparté’s own no longer do.
Also exported
Section titled “Also exported”createConversationManager— Svelte stores over the coreAparteConversationManager(list / create / archive), for a multi-conversation sidebar.
Testing it
Section titled “Testing it”Vitest — every runner — executes on Node, so @aparte/core resolves to its DOM-free
entry and no <aparte-*> element upgrades under jsdom: the tag stays a plain
HTMLElement and every assertion about it fails for a reason nothing explains. Alias the
specifier to @aparte/core/browser, the
entry with the elements in it.