This is the abridged developer documentation for aparté
# Compare aparté with Loquix, assistant-ui, AI Elements & more (2026)
> A fair, dated comparison of AI chat UI libraries — Loquix, assistant-ui, Vercel AI Elements, deep-chat, kitn, OpenAI ChatKit — on framework lock-in, runtime, local/in-browser models, human-in-the-loop and theming.
If you are choosing a chat UI library for an LLM product, the libraries below are the ones you will shortlist. This page says what each is good at and where aparté differs — including the cases where you should pick the other one. Claims about other projects were checked against their public docs in **August 2026**; they move fast, so verify before deciding. The short version: **aparté is the one that is a complete runtime *and* framework-agnostic *and* free of third-party dependencies.** The React kits have more components. The other web-component kits are presentational — you bring the loop. If you need neither framework freedom nor a runtime, you have better options than aparté, and they are named below. ## The matrix [Section titled “The matrix”](#the-matrix) | | aparté | assistant-ui | AI Elements | deep-chat | Loquix | kitn | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------- | ----------------------------------- | ------------------------------- | ----------------------------------- | ----------------------------- | | Frameworks | Web Components + React, Vue, Svelte, Angular wrappers | React | React | Web Component (+ React wrapper) | Web Component (Lit) | Web Component | | Runtime included (streaming, agent loop, tool calls) | **Yes** — [core](/why/) + [headless engine](/guides/engine/) | Runtime adapters (AI SDK, LangGraph, …) | Via the Vercel AI SDK | Built in, config-driven | No — presentational | No — presentational | | Framework you must adopt | **None** — and no third-party dependency in `@aparte/core` | React | React + the AI SDK | None | None (Lit is bundled) | None | | Human-in-the-loop: tool approval, typed questions | **Approval at the composer, [typed elicitation panel](/guides/elicitation/)** | Generic tool UI | Confirmation component | — | Rendered, not run | Rendered, not run | | Edit / retry / branch a conversation | **[Yes, with a version picker](/guides/conversations-branching/)** | Yes | Partial | — | Partial | — | | Persistence | [A storage interface you implement](/guides/conversation-persistence/) | A hosted cloud service (paid) | Your own | — | — | — | | Theming | [CSS variables, light DOM](/guides/theming/), no fork | Tailwind / shadcn — fork the markup | shadcn — the code lives in your app | Config object | CSS variables | CSS variables + online editor | | Run a model in the browser | Yes — the [transformers provider](/providers/ai/transformers/) | — | — | Yes (Web LLM) | — | — | | Scripted demo/test provider | **Yes — [the scenario provider](/providers/ai/scenario/)** (deterministic turns: thinking, tools, artifacts) | — | — | Demo mode (canned) | — | — | | Markdown, code, reasoning | Yes ([opt-in plugins](/plugins/)) | Yes | Yes | Yes | Yes | Yes | | Mermaid, KaTeX | — | Yes | Yes | — | — | — | | Voice | — | Yes (realtime) | Yes | Yes | — | — | | Citations / sources | — | Partial | Yes | — | Yes | — | | Floating / modal chat | — | Yes | — | Yes (embed) | Partial | — | | Best fit for | Multi-framework teams that want the runtime included | React teams, all-in on React | Apps already on the Vercel AI SDK | A config-driven embed, fast | A polished presentational kit (Lit) | A minimal presentational kit | | Licence | MIT | MIT | Apache-2.0 | MIT | MIT | MIT | The rows read left to right as *what aparté is for*: the top half is the runtime, the bottom half is the catalogue. aparté wins the top half and loses the bottom half, and both halves are true. ## assistant-ui [Section titled “assistant-ui”](#assistant-ui) **The React reference.** A composable primitives kit (thread, composer, message, action bar, branch picker, chain of thought, attachments) with the widest set of runtime integrations in the category — the Vercel AI SDK, LangGraph, LangChain, Mastra, AG-UI, A2A and more — plus realtime voice, Mermaid and KaTeX rendering, and a hosted cloud for thread persistence. **Pick assistant-ui when** you are on React for good, want its idioms end to end, and want to plug into one of those runtimes rather than run a loop of your own. Its ecosystem is larger than aparté’s and will stay so. **Pick aparté when** the same chat has to ship in more than one framework (or none), when you do not want a UI kit to dictate your styling system, or when you want a runtime in the library rather than an adapter to someone else’s. Theming is the other line: assistant-ui is Tailwind/shadcn, so a visual change is a fork of markup; aparté is CSS variables on light DOM, so it is a stylesheet. ## Vercel AI Elements [Section titled “Vercel AI Elements”](#vercel-ai-elements) **The largest catalogue.** Fifty-odd shadcn components — chatbot, code (artifact, file tree, terminal, sandbox, web preview), voice and workflow-graph families — installed by CLI, so the source lands in your app. Requires React and the Vercel AI SDK. **Pick AI Elements when** you build on Next.js and the AI SDK and want a finished part for every screen an agent product has. Nobody else has that breadth. **Pick aparté when** you are not on React, or when you want the parts to update as a dependency rather than as copied source. One thing to know: AI Elements’ file tree, terminal and test results are **app-fed views**, not message types — the same conclusion aparté reached when it removed those segment kinds. A tool result rendered richly — generative UI, in the term the field settled on — is the seam both libraries converge on. ## deep-chat [Section titled “deep-chat”](#deep-chat) **The fastest time-to-chat.** One `` element, configured by attributes and objects, connects directly to twenty-odd vendor APIs, does files, camera, microphone, speech in and out, and can run a model in the browser. **Pick deep-chat when** you want one tag and a config object and you are done — it is the best of the category at that, and the only other one that runs a model client-side. **Pick aparté when** you need composable parts rather than one element (aparté is a viewport, a composer, bubbles and a status line you place yourself), token-level theming, an agent loop with [tool approval](/guides/tools/), or the same chat in four frameworks with [typed wrappers](/frameworks/). deep-chat’s customisation is a large config surface; aparté’s is CSS and render hooks. ## Loquix [Section titled “Loquix”](#loquix) **A presentational web-component kit.** A few dozen Lit components across reasoning, trust, search and template categories, with accessibility as its headline. By design it ships no provider, no loop and no transport: it is a type-only interface you feed. **Pick Loquix when** you already have a chat runtime you like and want a broad, accessible set of views to render its output. **Pick aparté when** you want the runtime too. aparté’s core has no third-party dependency (Loquix carries Lit), and its [engine](/guides/engine/) runs the agent loop, the tool calls, the approval gate and the conversation compactor — the parts a presentational kit leaves to you. Loquix’s catalogue is wider than aparté’s; its roadmap points at provider integrations, which is the ground aparté already covers. ## kitn [Section titled “kitn”](#kitn) **The presentational twin.** A single custom element that renders tool calls, reasoning traces and typed cards (confirmations, choices, forms) — “you bring the model” is the stated contract. Its theming is its strength: a small token set, dozens of presets and an online theme editor that exports CSS. **Pick kitn when** you want a drop-in view over a loop you run elsewhere, and a theme in five minutes. **Pick aparté when** the typed cards have to be *honoured*, not just drawn: aparté’s [elicitation](/guides/elicitation/) pauses the run, collects the answer and resumes it, and the approval step gates a real tool call. kitn draws the same vocabulary without the runtime under it. ## OpenAI ChatKit [Section titled “OpenAI ChatKit”](#openai-chatkit) **The platform drop-in.** A web component (with a React wrapper) served by the OpenAI platform through a client token — streaming, tools, attachments and chain-of-thought visualisation without writing the front end. Self-hosting goes through their Python SDK. **Pick ChatKit when** you build on the OpenAI platform and want their hosted chat experience with the least code. **Pick aparté when** the model is not OpenAI’s, when the key must stay yours (browser-direct BYOK or [your own endpoint](/guides/backend-transport/)), when a model runs locally, or when you need theming beyond what a hosted surface exposes. The lock-in there is the vendor; aparté’s [transport](/why/) is the seam that keeps it out. ## Also in the field [Section titled “Also in the field”](#also-in-the-field) * **CopilotKit** — a category above: app-state sync with agents, generative UI, LangGraph co-agents. Not a chat component; if that is what you need, aparté is not it. * **NLUX** — React and vanilla, with a modified MPL licence to read before adopting. * **TanStack AI** — type-safe, framework-agnostic hooks with no UI (alpha). Closer to aparté’s engine than to its components. * **Chainlit** — Python full-stack; a different stack entirely. ## How to read this page [Section titled “How to read this page”](#how-to-read-this-page) aparté is alpha (`0.x`, released in lockstep, renames land as renames — see [Why aparté](/why/#where-it-is-in-its-life)). The comparison is written against that version; the catalogue rows will change as components land, and the runtime rows are the ones that define the project. If a claim above is wrong or out of date, [open an issue](https://github.com/apartejs/aparte/issues) — a comparison that flatters the author is worth nothing to the reader.
# Open Source ChatGPT Clone UI, Assembled with aparté
> A complete chat application in Web Components — sidebar with conversation list and search, header, chat with attachments — talking to a scripted model. Copy it, point it at your model.
The whole application, assembled from what ships: the shell grid and the header are **recipes** (classes), the sidebar and the conversation list are **elements** (they have behaviour — the drawer, the search, the selection), and the chat is ``. It talks to a **scripted model** here — no key, no network — so everything you see run is real except the model: the loop, the transcript, the tool call, the attachments. Width 375 768 Full [Open in a tab](/preview/demo/example-chatgpt-like/) [A ChatGPT-like assistant, assembled](/preview/demo/example-chatgpt-like/) Type haiku, table, code or weather. Narrow the frame and the sidebar becomes a drawer behind the toggle. Open it in a tab for the full screen.
```html
(aparté)
What is aparté?
scripted model
```
## What you are looking at [Section titled “What you are looking at”](#what-you-are-looking-at) * **`.aparte-app-shell`** — a two-column grid: the sidebar spans both rows, the header and the main area take the second column. Give it a height and it fills it. * **``** — collapsible, a drawer under its `breakpoint`, and it listens for `data-aparte-sidebar-toggle` and `data-aparte-sidebar-search` on its own, so the header’s button and the search field need no script. * **``** — takes a `conversations` array, marks `active-id`, and dispatches `aparte-conversation-select`. The header title above follows it in one listener. * **``** — the chat with the file picker; the host consumes the files (an `AparteClient` does). ## Make it yours [Section titled “Make it yours”](#make-it-yours) Three moves turn this into your application: 1. **The model.** Replace the scripted provider with yours — an OpenAI-compatible endpoint (`@aparte/provider-openai-compat`, presets for OpenAI, Mistral, OpenRouter, Ollama, LM Studio), the Vercel AI SDK, or a model in the browser (`@aparte/provider-transformers`) — or keep the key on your server with the [backend transport](/guides/backend-transport/). 2. **The conversations.** Feed `conversations` from your store and persist with the [conversation manager](/guides/conversation-persistence/). 3. **The look.** It is all CSS variables and light DOM — see [theming](/guides/theming/). The recipe behind the shell, and its mobile behaviour, are on the [app shell guide](/guides/app-shell/); the layouts a chat can take — split, feed, side panel — on the [layout guide](/guides/layout/).
# AI Chat Components for React, Vue, Svelte & Angular
> Use aparté from React, Vue, Svelte or Angular — thin wrappers over the framework-agnostic web components, with ergonomic components plus a generic escape hatch.
`@aparte/core` is vanilla web components, so it already works in **any** framework. The framework packages add ergonomics on top — you don’t have to hand-wire refs, events and lifecycle yourself. Each wrapper ships **three layers**: * **An opinionated component** — e.g. React’s ``: the full chat surface (viewport + composer + slots) as one idiomatic component, plus hooks/stores/services for state and the client. * **A typed surface for every element** — see [Placing elements, typed](/frameworks/elements/). Real attributes and real events on the real tag, checked by your compiler: JSX intrinsics in React, `GlobalComponents` in Vue, `SvelteHTMLElements` in Svelte, a standalone directive per element in Angular. This is how you place a model selector, a conversation list, or a composer you compose yourself. * **A generic escape hatch** — e.g. React’s ``: mounts **any** custom element as a framework component (props + events forwarded). It takes a tag name, not a registry lookup, so an element of your own or a third party’s works the same way; whatever defines the element has to be imported, and until it is the tag mounts empty and inert. For aparté’s own elements, reach for the typed surface instead. The wrappers depend **only** on `@aparte/core` — never on a specific provider. You register a provider (or none) in the [config](/providers/); the wrapper streams whatever’s configured. See [Providers](/providers/) for the model side. ## React, Vue, Svelte and Angular chat components [Section titled “React, Vue, Svelte and Angular chat components”](#react-vue-svelte-and-angular-chat-components) * **[React chat component](/frameworks/react/)** — `@aparte/react` (React 18/19). * **[Vue chat component](/frameworks/vue/)** — `@aparte/vue` (Vue 3.5+). * **[Svelte chat component](/frameworks/svelte/)** — `@aparte/svelte` (Svelte 4 and 5). * **[Angular chat component](/frameworks/angular/)** — `@aparte/angular` (Angular 19, standalone). All four expose the **same slots and the same six callbacks** — `messageSent`, `action`, `messagesChange`, `messageAppended`, `typingChange`, `conversationCreated` — each in its own idiom, and that is checked mechanically rather than promised. Both tables, with the four syntaxes side by side, are generated from the wrapper source: [Wrapper surface](/reference/wrappers/).
# Angular AI chat component, standalone — @aparte/angular
> The @aparte/angular wrapper — an ergonomic standalone component plus services over the aparté web components.
`@aparte/angular` wraps `@aparte/core` for Angular 19: an ergonomic [``](/components/conversation/aparte-chat/) standalone component, services for the client and conversations, a typed directive for every element, and a generic `` escape hatch.
```bash
npm install @aparte/angular @aparte/core @angular/core @angular/common rxjs
```
`@aparte/core`, `@angular/core`, `@angular/common` and `rxjs` are **peer dependencies**. Angular 19 only, deliberately The peer range is `^19.2.0` because 19 is the only major this wrapper is built and browser-tested against. It is a thin bridge over standard custom elements — no private Angular API, no `NgModule` — so 20 through 22 will very likely work, and an override or `--legacy-peer-deps` will let you try. We do not widen the range on “very likely”: [`@aparte/svelte`](/frameworks/svelte/) claims two majors because each is compiled and driven in a real browser in CI, and Angular has no second example yet. Tell us if you need a newer major — that is what would justify building one. ## `` [Section titled “\”](#aparte-chat) The components are **standalone** — import them directly, no NgModule:
```ts
import { Component } from '@angular/core';
import { AparteChatComponent, type AparteMessage } from '@aparte/angular';
import '@aparte/core/styles.css';
@Component({
standalone: true,
imports: [AparteChatComponent],
template: `
Ask me anything…
`,
})
export class Chat {
// The chat owns its thread. Observe it via (messagesChange) — do NOT push it
// back through [messages]: the user's message is appended for you on send, so
// re-adding it in a (messageSent) handler double-counts it.
messages: AparteMessage[] = [];
}
```
Slots are **content projection** by attribute: `[slot='empty-state']`, `[slot='composer']`, `[slot='above-composer']`, `[slot='toolbar']` — the last one being the composer’s bottom row, with an example under [The composer toolbar](/guides/customization/#the-composer-toolbar). For a fully custom bubble, pass a template instead:
```html
{{ message.content }}
```
Outputs: `messageSent`, `messagesChange`, `messageAppended`, `action`, `typingChange`, `conversationCreated` — the same six on all four wrappers, with the payloads and the other three syntaxes side by side in the generated [Wrapper surface](/reference/wrappers/#callbacks). The imperative API (streaming, branch/edit, `scrollToBottom`, `getViewport`) is on the component instance — grab it with a `@ViewChild`. `injectTokenStream` takes the cross-wrapper `AsyncIterable` — the exact call that works on React/Vue/Svelte — **or** an RxJS `Observable` (the Angular-idiomatic shape); everything else mirrors the other wrappers. ## Wiring a real model [Section titled “Wiring a real model”](#wiring-a-real-model) The wrapper is **provider-agnostic**. `provideAparte()` registers your providers and client options at bootstrap **and starts the client** (`autoConnect`, on by default) — composer sends stream replies with zero extra wiring: main.ts
```ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AparteDirectTransport, aparteGlobalConfig } from '@aparte/core';
import { createOpenAICompatProvider, presets } from '@aparte/provider-openai-compat';
import { provideAparte } from '@aparte/angular';
aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));
bootstrapApplication(App, {
providers: [
provideAparte({
providers: [createOpenAICompatProvider(presets.OPENROUTER)],
clientOptions: { /* AparteClientOptions */ },
}),
],
});
```
That’s it — no lifecycle wiring in your components. To own the client lifecycle yourself, pass `autoConnect: false` and use the service:
```ts
// only with autoConnect: false — the manual escape hatch
import { inject } from '@angular/core';
import { AparteAiService } from '@aparte/angular';
export class Chat {
private ai = inject(AparteAiService);
ngOnInit() { this.ai.connect(); } // idempotent — safe even if already connected
ngOnDestroy() { this.ai.disconnect(); }
}
```
`provideAparte()` is **config sugar and fully optional** — the components work without it, and you can call `aparteGlobalConfig.*` directly exactly like the React/Vue/Svelte wrappers do. Its `plugins` slots take **objects or loader functions** you supply, and `locale` takes an `AparteLocale` **object** (e.g. `locale: fr` from `@aparte/locale-fr`) — none of them take package-name strings — so this package stays a leaf with no plugin catalog. Pass a per-instance `[config]` to scope providers/transport to a single `` instead of `aparteGlobalConfig`. ## Any aparté element: a typed directive [Section titled “Any aparté element: a typed directive”](#any-aparté-element-a-typed-directive) Every element has a standalone directive whose selector is the tag, so you write the real element with typed Inputs and one Output per event — and **no `CUSTOM_ELEMENTS_SCHEMA`**, which would switch template checking off for every unknown tag in the file. Import the ones you use, or `APARTE_ELEMENT_DIRECTIVES` for all of them:
```ts
import { AparteSelectDirective } from '@aparte/angular';
// then: @Component({ imports: [AparteSelectDirective], … })
```
```html
@if (showPicker) {
}
```
The `@if` is the point: the element is really in the template, so control flow and content projection reach it. Full set and the rules on [Placing elements, typed](/frameworks/elements/). ## Any OTHER element: `` [Section titled “Any OTHER element: \”](#any-other-element-aparte-ui) For an element aparté does not define — one of yours, or a third party’s — mount it generically. It forwards the interactive aparté events by default; pass `[events]` to listen to others:
```html
```
This used to be how you placed a model selector. It still works, and it is still the only way to mount a tag aparté knows nothing about — but for aparté’s own elements the directive above gives you type checking, one output per event, and an element the template can actually wrap. ## Also exported [Section titled “Also exported”](#also-exported) * `ConversationManagerService` — signal-based view over the core `AparteConversationManager` (list / create / archive), for a multi-conversation sidebar. ## Testing it [Section titled “Testing it”](#testing-it) Vitest, Karma — every runner — executes on Node, so `@aparte/core` resolves to its DOM-free entry and no `` 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`](/frameworks/elements/#testing-your-components), the entry with the elements in it.
# Placing elements, typed
> Every aparté element has a typed surface in all four frameworks — real attributes, real events, checked by your compiler. Plus the escape hatch for an element aparté does not define.
`` gives you the whole turn in one tag. Everything else — the model selector, the conversation list, a composer you compose yourself — is a **custom element you place**, and this page is about placing those with your compiler on your side. ## What “typed” means here [Section titled “What “typed” means here”](#what-typed-means-here) Every **core** element’s attribute surface is declared once in `@aparte/core` and consumed by all four wrappers. The registry is `AparteElementAttributes`, one entry per tag, with `AparteElementTagName` as its key union:
```ts
import type { AparteElementAttributes, AparteElementTagName, AparteSelectAttributes } from '@aparte/core';
// One entry per tag. The wrappers derive their own typing from this, so an element added
// to core is typed in every framework the moment it lands here.
type SelectAttrs = AparteElementAttributes['aparte-select']; // = AparteSelectAttributes
type EveryTag = AparteElementTagName; // 'aparte-chat' | 'aparte-select' | …
const preset: AparteSelectAttributes = { placeholder: 'Pick a model', searchable: true };
```
The per-element interfaces are exported individually too, when you want to type your own wrapper around one: `AparteChatAttributes`, `AparteChatViewportAttributes`, `AparteChatBubbleAttributes`, `AparteChatStatusAttributes`, `AparteComposerAttributes`, `AparteComposerInputAttributes`, `AparteComposerActionAttributes`, `AparteComposerAddAttachmentAttributes`, `AparteComposerToolbarAttributes`, `AparteConversationListAttributes`, `AparteSelectAttributes`, `AparteOptionAttributes`, `AparteOptgroupAttributes`, `AparteProgressSpinnerAttributes`, and `AparteNoAttributes` for the four that observe nothing. An element that does **not** come from core — from a plugin, or one of yours — is not in this registry, and that is the boundary rather than an omission. See [Your own element, or a plugin’s](#your-own-element-or-a-plugins). ### The one thing to know about presence attributes [Section titled “The one thing to know about presence attributes”](#the-one-thing-to-know-about-presence-attributes) An aparté element is **attribute-driven**: it reacts to an attribute being present, not to a property being assigned. In a template that means `false` is the wrong value to write, because React, Vue and Svelte all stringify what they set on a custom element — `searchable={false}` renders `searchable="false"`, and code that tests `hasAttribute` reads that as **on**. So in those three, a presence attribute is `'' | null | undefined`, and the types enforce it. Write `''` to set and `null` (or `undefined`) to remove. `AparteTemplateAttrs` and `AparteAttrValue` are the mapping that does it, exported in case you build your own template integration:
```ts
import type { AparteTemplateAttrs, AparteAttrValue, AparteSelectAttributes } from '@aparte/core';
type InATemplate = AparteTemplateAttrs;
// searchable?: '' | null | undefined ← not boolean, on purpose
// placeholder?: string
type Presence = AparteAttrValue; // '' | null | undefined
```
Angular is the exception, and its directives take a real `boolean` — see below. ## React [Section titled “React”](#react) The `aparte-*` tags are typed JSX intrinsics as soon as you import from `@aparte/react`. Nothing to register:
```tsx
// A composer you compose yourself, slotted into .