51 components for AI chat and agents, built as Web Components
The transcript, the tools, the approvals and the composer of an AI app, streaming any model or agent — in React, Vue, Svelte, Angular or vanilla JS, with zero third-party dependencies.
Between your framework and your model
Every piece of an AI chat
24 chat components, each on its own page with live variants: the transcript, everything a turn can hold, the person in the loop and the composer.
A person stays in the loop
An agent that acts asks first. The chat draws every step of that conversation, and the loop keeps it.
-
Approve each tool call
The call waits, its arguments on screen, until someone allows or rejects it.
-
Ask the person a question
The model asks with choices or a short form; the answer stays in the transcript.
-
Decide how much it does alone
Plan, ask before every change, or act: one switch in the composer.
-
Keep the loop on your server
A turn that waits for a person survives a reload and picks up where it stopped.
<!-- In your app, AparteClient asks this for any tool registered with needsApproval: true.
Here the request is made by hand: import { requestUserInput } from '@aparte/core'. -->
<aparte-chat style="height: 26rem; --aparte-elic-max-height: 20rem"></aparte-chat>
<script>
const { requestUserInput } = window.aparte;
const chat = document.querySelector('aparte-chat');
chat.viewport.appendMessage({ id: 'u1', role: 'user', content: 'Clean up the old client.', timestamp: Date.now() });
chat.viewport.appendMessage({
id: 'a1', role: 'assistant', timestamp: Date.now(),
segments: [{
id: 's1', type: 'tool_call', status: 'awaiting-approval',
toolCall: { id: 't1', name: 'delete_file', input: { path: 'src/legacy/old-client.ts' } },
}],
});
// The decision is asked at the composer, with the arguments it is about.
requestUserInput({
kind: 'approval',
message: 'Run delete_file?',
details: JSON.stringify({ path: 'src/legacy/old-client.ts' }, null, 2),
options: [
{ value: 'approve', label: 'Approve' },
{ value: 'reject', label: 'Reject', tone: 'deny' },
],
}).catch(() => {});
</script>Two lines in your framework
The same elements in React, Vue, Svelte, Angular or vanilla JS: install the core and place the chat.
import '@aparte/core';
import '@aparte/core/styles.css';<aparte-chat></aparte-chat>import { AparteChat, useAparteChat } from '@aparte/react';
import '@aparte/core/styles.css';
export function Chat() {
const chat = useAparteChat();
return <AparteChat ref={chat.ref} messages={chat.messages} onMessagesChange={chat.setMessages} />;
}<script setup lang="ts">
import { AparteChat, useAparteChat } from '@aparte/vue';
import '@aparte/core/styles.css';
const chat = useAparteChat();
</script>
<template>
<AparteChat :ref="chat.chatRef" :messages="chat.messages.value" @messages-change="chat.onMessagesChange" />
</template><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} onmessagesChange={(m) => chat.onMessagesChange(m)} />import { Component } from '@angular/core';
import { AparteChatComponent, type AparteMessage } from '@aparte/angular';
import '@aparte/core/styles.css';
@Component({
standalone: true,
imports: [AparteChatComponent],
template: `<aparte-chat (messagesChange)="messages = $event"></aparte-chat>`,
})
export class Chat {
messages: AparteMessage[] = [];
}The four wrappers are published, typed, and driven in a browser by the same suite as the plain build. The framework guides · Models and agents
Make it yours
The same chat three times. Tokens set the palette and the shapes, an icon set and an avatar provider change the glyphs, and since nothing hides in a shadow root, a plain selector reaches the rest.
<!-- Nothing set: the palette, the shapes and the glyphs core ships, light or dark with the page. -->
<style>
.skin { padding: 1rem; background: var(--aparte-bg); }
</style>
<div class="skin">
<aparte-chat style="height: 28rem">
<aparte-chat-viewport>
<aparte-chat-bubble message-id="u1" data-role="user" content="Can it match our brand?"></aparte-chat-bubble>
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant" content="Yes: colours, shapes, fonts, icons and avatars are all yours to set."></aparte-chat-bubble>
</aparte-chat-viewport>
<aparte-composer placeholder="Ask anything">
<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>
</div><!-- A messenger: a bubble on both sides, a blue fill, round shapes and a round send,
avatars, and a filled paper plane. -->
<style>
.messenger {
--aparte-primary: #2563eb;
--aparte-primary-hover: #1d4ed8;
--aparte-bg: #eef2f9;
--aparte-surface-1: #ffffff;
--aparte-surface-2: #e3e9f4;
--aparte-text: #101828;
--aparte-text-muted: #667085;
--aparte-border: #d9e0ec;
--aparte-radius-unit: 7px;
--aparte-font-family: ui-rounded, 'SF Pro Rounded', 'Segoe UI', system-ui, sans-serif;
}
/* A token core derives from the base is set where core declares it: on the chat. */
.messenger aparte-chat {
--aparte-radius-send-btn: 999px;
--aparte-message-content-bg-user: var(--aparte-primary);
--aparte-message-content-bg-assistant: #ffffff;
}
/* A token can be scoped to one part: the person's words in white on the blue. */
.messenger .aparte-message[data-role="user"] {
--aparte-content-color: #ffffff;
}
/* A plain selector reaches any part: the assistant gets a bubble too. */
.messenger .aparte-message[data-role="assistant"] .aparte-message-content {
width: fit-content;
padding: var(--aparte-message-content-padding);
}
.skin { padding: 1rem; background: var(--aparte-bg); }
</style>
<script>
const { aparteGlobalConfig } = window.aparte;
// Avatars are a function that fills the avatar's element: initials here.
aparteGlobalConfig.setAvatarProvider({
render(role, host) { host.textContent = role === 'user' ? 'MO' : 'AI'; },
});
// An icon set is an object of SVG strings, one per glyph it replaces.
aparteGlobalConfig.setIconProvider({
send: () => '<svg class="aparte-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M3.4 20.4 21 12 3.4 3.6 3.3 10l12.6 2-12.6 2z"/></svg>',
});
</script>
<div class="skin messenger" data-aparte-theme="light">
<aparte-chat style="height: 28rem">
<aparte-chat-viewport>
<aparte-chat-bubble message-id="u1" data-role="user" content="Can it match our brand?"></aparte-chat-bubble>
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant" content="Yes: colours, shapes, fonts, icons and avatars are all yours to set."></aparte-chat-bubble>
</aparte-chat-viewport>
<aparte-composer placeholder="Ask anything">
<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>
</div><!-- A terminal: dark and square, one monospaced face, no bubbles, the person's line a
prompt, and a return key to send. -->
<style>
.terminal {
--aparte-primary: #3fb950;
--aparte-primary-hover: #56d364;
--aparte-bg: #0b0f14;
--aparte-surface-1: #10161e;
--aparte-surface-2: #18202b;
--aparte-text: #d7e2ee;
--aparte-text-muted: #7d8a99;
--aparte-border: #233041;
--aparte-radius-unit: 0px;
--aparte-font-family: ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace;
--aparte-font-scale: 0.95;
}
/* A token core derives from the base is set where core declares it: on the chat. */
.terminal aparte-chat {
--aparte-message-content-bg-user: transparent;
--aparte-message-content-padding: 0;
}
/* Plain selectors: the person's line becomes a prompt, the names go quiet. */
.terminal .aparte-message[data-role="user"] .aparte-message-content {
display: flex;
gap: 1ch;
}
.terminal .aparte-message[data-role="user"] .aparte-message-content::before {
content: "$";
color: var(--aparte-primary);
}
.terminal .aparte-name {
color: var(--aparte-text-muted);
text-transform: lowercase;
}
.terminal .terminal-return {
font: 600 1.1rem var(--aparte-font-family);
}
.skin { padding: 1rem; background: var(--aparte-bg); }
</style>
<script>
const { aparteGlobalConfig } = window.aparte;
// An icon may be any markup: a return key in place of the plane.
aparteGlobalConfig.setIconProvider({
send: () => '<span class="terminal-return" aria-hidden="true">⏎</span>',
});
</script>
<div class="skin terminal" data-aparte-theme="dark">
<aparte-chat style="height: 28rem">
<aparte-chat-viewport>
<aparte-chat-bubble message-id="u1" data-role="user" content="Can it match our brand?"></aparte-chat-bubble>
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant" content="Yes: colours, shapes, fonts, icons and avatars are all yours to set."></aparte-chat-bubble>
</aparte-chat-viewport>
<aparte-composer placeholder="Ask anything">
<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>
</div>The rest of your app wears the same tokens: a kit of 22 recipes — buttons, fields, menus, dialogs, tabs. Theme it · The kit
Start from a template
Whole apps built only from aparté, running. Open one, then take its code.
-
Assistant
A ChatGPT-style assistant: conversations in a sidebar, Markdown and code, reasoning, web search, versions of a reply, files and a model picker.
View the code ↗ -
Agent workshop
A coding agent beside its workspace: it reads, edits and tests a repository, asks before it writes, and shows the file, the terminal and the diff.
View the code ↗ -
Support widget
A help chat on a product’s website: a launcher, a compact panel, help-center articles as cards, a question with choices, and a handoff to a person.
View the code ↗ -
Research assistant
An agent that plans, searches, reads and writes: source cards in the chat, a cited report with a chart and a table in the pane beside it.
View the code ↗
Checked, not claimed
A beta: the public surface is frozen, and a change to it ships with a notice. Every figure here is read from the repository when the site builds.
What a working chat weighs
What each bar holds, and how it is measured
Each library set up from its own docs' quickstart.
- Loquix 0.6.0: 26 KB + 1 KB CSS
- Presentational components: the six its Quick Start imports and the token sheet, plus about 50 lines of ours to stream a reply, since it ships no model client; replies are plain text. The full import its README shows: 82 KB + 1 KB CSS.
- aparté 0.19.2: 86 KB + 20 KB CSS
- The chat with Markdown while it streams, its own client for any OpenAI-compatible API, and the agent loop with tools and approvals.
- Deep Chat 2.5.1: 120 KB
- One element with its own clients for 24 AI services (OpenAI, Claude, Gemini, Ollama…), Markdown and tool calls; its styles are inside its JavaScript. The CDN script its docs also list, served as it ships: 105 KB.
- assistant-ui 0.15.29: 377 KB + 67 KB React
- The AI SDK runtime and the Thread its docs copy into your app: Markdown, tool calls, attachments; the model is called from your server, and its CSS is your Tailwind build’s output. Its LocalRuntime with an adapter you write, in place of the AI SDK: 299 KB + 67 KB React.
- CopilotKit 1.79.1: 682 KB + 18 KB CSS + 67 KB React
- Its documented React chat: Markdown, math, code highlighting, tool calls and generative UI; its runtime runs on a server and is not counted.
Measured on 16 October 2026 with esbuild 0.27.7: minified, code splitting on, what loads at startup, gzip level 9; every library at its latest version published before 17 October 2026. Loquix and Deep Chat keep their component styles inside their JavaScript; assistant-ui's CSS comes out of your own Tailwind build and is not counted. CopilotKit's build also splits off 2.9 MB of optional chunks (a grammar per highlighted language, diagrams, a developer inspector), fetched only when a reply needs one. A server is never counted. The script that measures it ↗
- 0 third-party dependencies
- in @aparte/core. Markdown, highlighting and the rest are plugins you add.
- 3,494 tests
- in 353 files, green on the last run.
- 3 browser engines
- drive the examples end to end: Chromium, WebKit, Firefox.
- axe on every example
- an accessibility scan in the end-to-end suite, idle and mid-turn.
Start with one element
Install the core, drop a chat in a page, point it at a model.