Skip to content

Customization

Theming covers the look through CSS variables. This page is the structure and behaviour: when a colour isn’t enough — a custom typing indicator, your own attachment chip, an avatar, extra buttons, a brand-new block type — you reach for render hooks and the action registry.

Everything here goes through the global AparteConfig (or a scoped instance — see Per-instance config). Nothing requires forking a component.

import { AparteConfig } from '@aparte/core';

A render hook replaces one region’s markup. Each returns string | HTMLElement — return a string for simple markup (inserted as innerHTML), or an HTMLElement to attach your own listeners/framework nodes with no innerHTML XSS surface.

HookReplacesReceives
setStatusRendererthe typing indicator(text)
setErrorRendereran error segment({ message, … })
setAttachmentRendereran attachment chip(attachment)
setSiblingNavRendererthe branch picker ‹ 1/2 ›({ count, index })
setBubbleShellRendererthe whole bubble shell(ctx)
AparteConfig.setStatusRenderer((text) => `<div class="my-typing">${text}…</div>`);
AparteConfig.setErrorRenderer(({ message }) => {
const el = document.createElement('div');
el.className = 'my-error';
el.textContent = message; // textContent → no interpolation XSS
return el;
});
AparteConfig.setAttachmentRenderer((att) => {
const el = document.createElement('span');
el.className = 'my-chip';
el.textContent = att.name; // textContent → the filename can't inject HTML
return el;
});

Pass null to any setter to restore the default.

There’s no message avatar by default — the slot only appears once you provide one. The avatar provider is imperative: you get the already-sized .aparte-avatar host and fill it. Return an optional cleanup function for live components.

AparteConfig.setAvatarProvider({
render(role, host) {
host.textContent = role === 'assistant' ? '' : '🙂';
// return () => { /* dispose a mounted component */ };
},
});

Buttons on the message bubble and the composer come from one registry, keyed by zone. Add your own with registerAction:

AparteConfig.registerAction({
id: 'share',
icon: '<svg>…</svg>', // raw HTML if it starts with '<', else an icon key
label: 'Share',
zones: ['bubble'], // 'bubble' | 'composer' | both
bubble: { roles: ['assistant'] },
});

Clicks are declarative — they emit aparte-action, so you handle them in one place:

document.addEventListener('aparte-action', (e) => {
const { actionId, zone, messageId, role } = e.detail;
if (actionId === 'share') {/* … */}
});
  • zones decides where it shows; composer: { position: 'left' | 'right' } and bubble: { roles: [...] } refine placement; order sorts custom actions.
  • An onClick(event) callback is optional and fires alongside the event.
  • Hide/show at runtime with AparteConfig.setActionHidden(id, hidden).
  • The built-in bubble actions (copy / retry / edit / feedback) are toggled per role with AparteConfig.setBubbleActions({ … }).

Streamed replies are split into typed segments (text, code, thinking, terminal, …). Register a renderer to add your own type — a chart, a map, a form:

import { registerSegmentRenderer } from '@aparte/core';
registerSegmentRenderer({
type: 'chart',
render(segment) {
const el = document.createElement('div');
el.className = 'my-chart';
// build from segment data…
return el; // string or HTMLElement
},
});

Every icon ships as a zero-dependency inline SVG. Override any of them — with an SVG, an icon-font element, an emoji, or an <img> (the value is treated as trusted markup):

AparteConfig.setIconProvider({
copy: () => '<svg>…</svg>',
send: () => '<i class="fa fa-paper-plane"></i>',
});

You only override the keys you pass; the rest keep their defaults. The inline message editor’s save/cancel buttons use the check and close keys, so they follow your provider too; their colours are the --aparte-success (save) and --aparte-error (cancel) CSS variables.

Core is zero-dependency by default, so Markdown and syntax highlighting are off until you inject a renderer — keeping the bundle honest:

import { marked } from 'marked';
import { codeToHtml } from 'shiki';
AparteConfig.setMarkdownProvider((raw) => marked.parse(raw) as string);
AparteConfig.setHighlightProvider((code, lang) => codeToHtml(code, { lang, theme: 'dracula' }));

AparteConfig is global — right for the common one-chat-per-app case. To run several independently-customized chats on one page, attach an instance config to each chat’s root; every <aparte-*> inside resolves the nearest boundary and falls back to global.

import { AparteConfigClass, attachConfig } from '@aparte/core';
const support = new AparteConfigClass();
support.setStatusRenderer((t) => `<em>${t}</em>`);
attachConfig(document.querySelector('#support-chat')!, support);