Getting started
@aparte/core is a set of framework-agnostic web components for AI chat. You
drop <aparte-*> elements onto a page, stream tokens into them, and style
everything with CSS variables — no framework, and zero runtime dependencies.
By the end of this page you’ll have a working chat that streams a reply, running entirely in the browser.
Install
Section titled “Install”npm install @aparte/coreRegister the components
Section titled “Register the components”Import the package once (it registers the <aparte-*> custom elements), pull in
the stylesheet, and call registerDefaultRenderers().
import '@aparte/core'; // registers the <aparte-*> custom elementsimport '@aparte/core/styles.css'; // theme variables + component stylesimport { registerDefaultRenderers } from '@aparte/core';
registerDefaultRenderers(); // turns raw text/markdown into rendered bubblesAdd the markup
Section titled “Add the markup”<aparte-chat> is the container. It lays out a message viewport and a composer
as a flex column, and — with center-empty — keeps the composer centered as a
welcome state until the first message, then slides to the normal layout. Size it
(a height, or let it fill a parent) and you’re done.
Left empty, it fills in a default composition:
<aparte-chat center-empty placeholder="Say something…" style="height: 600px"></aparte-chat>Need full control — a custom composer, extra buttons? Put your own primitives
inside the same element. It still lays them out and runs center-empty, so you
keep the behaviour without a hand-written container:
<aparte-chat center-empty style="height: 600px"> <aparte-chat-viewport></aparte-chat-viewport>
<aparte-composer> <div class="aparte-composer-shell"> <div class="aparte-composer-row"> <aparte-composer-input placeholder="Say something…" style="flex:1"></aparte-composer-input> <aparte-composer-send></aparte-composer-send> </div> </div> </aparte-composer></aparte-chat>center-empty is opt-in — drop it and the composer sits at the bottom from the
start. The .aparte-composer-shell / .aparte-composer-row helpers give the
bordered input-with-a-send-button look; the composer itself is headless.
Make it stream
Section titled “Make it stream”The composer fires an aparte-send event when the user submits — it bubbles, so
you can listen on <aparte-chat>. Reach the message list via chat.viewport, add
the user’s message, then stream an assistant reply in.
Three viewport methods are all you need:
appendMessage({ id, role, content, timestamp })— create a bubbleappendToken(id, chunk)— stream text into it, token by tokencompleteMessage(id)— mark it done (stops the streaming caret)
const chat = document.querySelector('aparte-chat');const viewport = chat.viewport;
let n = 0;
// Stream a string into a fresh assistant bubble, a few characters at a time.function streamReply(text) { const id = 'a' + ++n; viewport.appendMessage({ id, role: 'assistant', content: '', timestamp: Date.now() });
const tokens = text.split(/(\s+)/); // keep the whitespace as its own tokens let i = 0; const timer = setInterval(() => { if (i >= tokens.length) { clearInterval(timer); viewport.completeMessage(id); return; } viewport.appendToken(id, tokens[i++]); }, 40);}
chat.addEventListener('aparte-send', (e) => { const text = e.detail.content;
// 1. Echo the user's message into the conversation. viewport.appendMessage({ id: 'u' + ++n, role: 'user', content: text, timestamp: Date.now() });
// 2. Reply. Here we fake it; next you'll wire a real model. streamReply(`You said: “${text}”. Now wire a transport to get a real answer.`);});(Using the primitives instead? Same code — just document.querySelector('aparte-chat-viewport')
for the viewport and listen on the composer.)
That’s a complete, working chat — no backend, no keys. Type a message and watch the reply stream in.
Wire a real model
Section titled “Wire a real model”Faking the reply is fine for a first look. To talk to a real LLM, you configure core
with two things and let AparteClient drive the streaming loop for you:
- A provider — the wire-format adapter for your model (an opt-in
@aparte/provider-*package), registered withAparteConfig.registerAIProvider(…). - A transport — where the request goes and how the key is handled:
DirectTransport— the browser talks to the provider directly (bring-your-own-key or a local model):AparteConfig.setTransport(new DirectTransport({ byok: true })).BackendTransport— the browser calls your endpoint, and the key stays server-side.
Once a provider and transport are set, construct an AparteClient and call .start() —
it then listens for aparte-send (and aparte-retry, aparte-edit) globally and streams
the assistant’s typed segments into your bubbles for you (no manual appendToken):
new AparteClient().start(); // .start() attaches the listeners — without it nothing streamsThe client owns the assistant turn; keep appending the user’s own message on
aparte-send as above — the framework wrappers do even that for you.
Provider adapters ship as opt-in @aparte/provider-* packages — see the
Providers section for the OpenAI-compatible adapter (OpenAI, Mistral, OpenRouter,
Groq, LM Studio, Ollama…), the Vercel AI SDK bridge (Anthropic, Google, 25+ vendors), and the
in-browser Transformers.js provider. You can also register any object implementing the
AparteAIProvider interface yourself.
Next steps
Section titled “Next steps”- Theming — restyle everything through CSS variables, no forking.
- Customization — icons, render hooks, action registries.
- Conversations & branching — retry / edit, branches, persistence.
- Providers — connect a real model: OpenAI-compatible, the AI SDK bridge, or in-browser Transformers.js.
- The agent engine — the headless
runStreamAgentloop and thestreamRunnerseam.