Add an AI chat to a Svelte app
Install @aparte/svelte, render <AparteChat>, stream a model with createAparteClient, and put your own UI in its slots — on Svelte 4 or 5, SvelteKit included.
Install
Section titled “Install”npm install @aparte/svelte @aparte/core
npm install @aparte/provider-scenario # a scripted model, to stream with no key@aparte/core and svelte are peer dependencies: Svelte ^4.0.0 || ^5.0.0. The
package ships its .svelte sources, which your build compiles, so one package serves both majors.
Render the chat
Section titled “Render the chat”<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>createAparteChat holds the messages in a store and mirrors the chat’s imperative API; connect
ties it to the component you bind. In a Svelte 5 component in runes mode, write the binding as
let comp = $state<AparteChatImperativeApi | null>(null); and the connection as
$effect(() => { chat.connect(comp); });. The person’s message is added when they send, so do not add
it yourself; onmessageSent fires after, for a side effect.
Stream a model
Section titled “Stream a model”// main.ts — registered once, before the app mounts
import { aparteGlobalConfig } from '@aparte/core';
import { createScenarioProvider } from '@aparte/provider-scenario';
import { showcase } from '@aparte/provider-scenario/showcase';
aparteGlobalConfig.registerAIProvider(createScenarioProvider({ scenarios: showcase }));
// …then mount your App.svelte<script lang="ts">
import { AparteChat, createAparteChat, createAparteClient, type AparteChatImperativeApi } from '@aparte/svelte';
const chat = createAparteChat();
createAparteClient();
const { messages } = chat;
let comp: AparteChatImperativeApi | null = null;
$: chat.connect(comp);
</script>
<AparteChat bind:this={comp} messages={$messages} onmessagesChange={(m) => chat.onMessagesChange(m)} />createAparteClient starts a client when the component mounts, stops it when it is destroyed, and
returns { client, abort }; it takes the AparteClientOptions. For a real model, register its
provider, name the model and give the key — aparteGlobalConfig.setModelConfig({ defaultProvider: 'openrouter', defaultModel: 'openai/gpt-4o-mini' }) and createAparteClient({ keyResolver })
(Connect a model). A config prop gives one <AparteChat> providers and a
transport of its own; give its client the same one, createAparteClient({ config }). With the
client running, turn on retry and edit with
aparteGlobalConfig.setBubbleActions({ retry: true, edit: true }) (Buttons).
Callbacks and events
Section titled “Callbacks and events”Each callback — onmessageSent, onaction, onmessagesChange, onmessageAppended,
ontypingChange, onconversationCreated — is a prop that gets the payload itself, and an event as
well: on:messagesChange={(e) => chat.onMessagesChange(e.detail)}, the payload under
event.detail. Both fire for the same occurrence, the callback first; write the callbacks on
Svelte 5.
Put your own UI in it
Section titled “Put your own UI in it”<AparteChat bind:this={comp} messages={$messages} onmessagesChange={(m) => chat.onMessagesChange(m)}>
<p slot="empty-state">Ask me anything…</p>
<svelte:fragment slot="toolbar"><span class="hint">Shift + Enter for a new line</span></svelte:fragment>
<svelte:fragment slot="bubble" let:message><MyBubble {message} /></svelte:fragment>
</AparteChat>| Slot | What it fills |
|---|---|
empty-state | the chat before the first message |
above-composer | the space above the composer |
toolbar | the composer’s bottom row, not drawn when you fill composer; <svelte:fragment slot="toolbar"> for several nodes (The composer toolbar) |
composer | the composer, replaced by yours |
bubble, with let:message | each message, drawn by you (Bubble) |
<AparteChat id="support" class="flex-1 min-h-0" loading={isFetchingHistory} attachments />The ones you reach for: placeholder, disabled and submitOnEnter; centerWhenEmpty;
loading, for a wait of your own; isTyping and typingText, a waiting line you drive yourself;
overlayComposer; attachments, off by default (Attachments);
elicitation={false} when you draw the questions yourself; conversationId, config and
layoutTransitionMs, how long the list holds its layout after a conversation swap. The root takes
class and a style string. id is the chat’s id, which every event carries: for two chats on one
page, each answered by its own client, give each an id and its client scopeToTargetId with it
(Wrapper surface).
Control it from code
Section titled “Control it from code”The chat store mirrors the chat’s imperative API — appendMessage, addSegment,
injectTokenStream, scrollToBottom, focusInput and the rest — to drive the chat from a loop of
yours (Your own loop). createConversationManager() lists, creates,
archives and pins saved conversations in stores once you call init(adapter) with your storage
adapter — and the chat’s config as second argument, if it has one — and the chat switches between
them through conversationId (Save conversations).
Any element, typed
Section titled “Any element, typed”<aparte-select searchable="" placeholder="Pick a model" on:aparte-select-change={(e) => (model = e.detail.value)}>
<aparte-option value="gpt-4o-mini">GPT-4o mini</aparte-option>
</aparte-select>Importing from @aparte/svelte declares every element of core in SvelteHTMLElements, so
svelte-check covers its attributes and its on: handlers: a presence attribute takes '' to be
on and null to be off (Elements, typed). In Svelte 5’s runes mode, write
the event as an attribute, typed the same way: onaparte-select-change={(e) => (model = e.detail.value)}.
A plugin’s element is typed by the plugin, and one of yours by you
(your own element). <AparteUi> mounts any
tag and sets its props; it forwards core’s events, so name your element’s own in events:
<AparteUi bind:this={ui} name="my-token-counter" props={{ 'data-budget': '8000' }} events={['token-count']} onelementEvent={(e) => (count = e.detail)} />onelementEvent gets the element’s own event, and on:elementEvent fires too, with it under
event.detail. Bound, it gives ui.getElement() and ui.callMethod(name, ...args).
SvelteKit and tests
Section titled “SvelteKit and tests”The wrapper has no server guard and is not tested under a server render: under SvelteKit, render
the chat on the client only — export const ssr = false in the route’s +page.ts, or a component
imported in onMount. @aparte/core itself imports on a server through its entry without elements
(On the server). Vitest runs on Node, so alias
@aparte/core to @aparte/core/browser for the elements to upgrade
(Testing your components).
A whole chat site in Svelte — the conversation sidebar, the header, settings, tools and the scripted model — is the Svelte 5 example, and the Svelte 4 one.