Skip to content
askdareDocumentation

    ADR-0008: UI customization layers and built-in themes

    • Status: Accepted
    • Date: 2026-10-29

    Context

    Users want two things at once:

    • Polished defaults for the "lazy" path.
    • Unlimited freedom to change the look.

    Hundreds of configuration options would freeze the library's API and still never be enough.

    Decision

    Customization layers

    Simple changes are configuration; hard changes are code. Logic (streaming, security, limits, protocol) always stays in the library.

    LayerMechanismVersion
    1. Configurationwidget: {...} in defineBot: typed, Zod-validated0.1
    2. Custom CSSwidget.css injected into the iframe. Library styles live in @layer askdare, so user CSS wins without !important. Stable parts via [data-ask-part] plus state attributes. The launcher is stylable from host CSS via ::part.0.1
    3. Host page JavaScriptaskdare('open' | 'close' | 'configure'), data-launcher="none" for a custom trigger, data-mode="inline"0.1
    4. Headless React@boldare/askdare/react: useChat(), primitives <Chat.*>, slots, toolRenderers0.2
    5. Ejectnpx @boldare/askdare add ui copies the default UI source into the user's repo, shadcn-style0.2

    Rules

    • Two kinds of configuration. Server config (model, instructions, tools, limits, secrets) and public config (widget) have separate types. Only the public part reaches the browser: the embed page (GET <basePath>/embed/<bot>) inlines it as JSON, and a unit test checks that the model, instructions, tools and vector store never appear in it.
    • About 15 semantic tokens (--ask-*); the rest of the palette is derived. Each derived accent is checked for contrast.
    • UI built as layers from day one: a headless core, primitives and a default skin. Layers 4 and 5 then only expose existing code.
    • Runtime overrides are allowlisted. askdare('configure') accepts only texts, starters and page context. Page context is off unless a bot opts in, and then travels in the visitor's own turn (user role) with a length cap, never in instructions (changed in step 4.1 after the security review: instructions outrank visitor text).
    • Accessibility cannot be configured away. Dev mode warns about weak contrast, and the focus ring is always visible.

    Built-in presets

    There are four presets, each with a light and a dark variant. Every preset is built only on the public styling API, which proves that layers 1 and 2 are enough, and each can be extended with defineTheme({ extends }).

    PresetCharacter
    Aura (default)Voice-first soft glass in Boldare's palette (coral, violet, teal): glossy bubble orb with a reflection, two-weight headline, card composer with a Send pill
    HaloClean, white to ice blue, frosted orb in rings of light, bold question, pill chips, round field with a soft blue mic
    SageA calm companion: sage and mint, light type, persona card with an "Online" pill, black pill CTAs, mic beside the field
    CareTrust-first and structured: indigo and lavender, mascot, capability checklist, Atkinson Hyperlegible, AAA text contrast

    Each preset is a set of tokens plus defaults for these component variants:

    • avatar
    • welcome (layout)
    • launcher (style)
    • bubbles
    • surface
    • density
    • composer

    Update (2026-10-29): Aura replaced Halo as the default, and Aura, Halo and Sage moved closer to their inspirations. Each preset now also has a few rules of its own in the widget stylesheet, written only with parts, tokens and variant attributes (:root[data-ask-preset="…"]), the same means a site has in widget.css.

    Implementation (step 3.6): 17 tokens (11 colors, the backdrop and orb paint, two font families, base size, radius); see Themes. The server renders a bot's theme as a content-hashed stylesheet (the iframe CSP forbids inline styles) and sets the variants as data-ask-* attributes on the iframe document. The requested bot's launcher look is inlined into widget.js?bot=<id> (only that bot's, so the script never lists other bots), and the launcher is drawn right on first paint without another request.

    Quality bar

    • Contrast validator in CI: AA for every token pair, and AAA for text in Care.
    • Fonts: OFL-licensed only, self-hosted, loaded lazily for the chosen preset, at most 80 KB per preset.
    • The orb is pure CSS: no WebGL, at most 3 KB.
    • Motion respects prefers-reduced-motion.
    • State gallery (site/app/gallery) feeds visual regression tests: 4 presets × light and dark × 3 widths.
    • A product designer reviews the presets before 0.1.0. Still to do as of 2026-10-30.

    Consequences

    • The styling contract (tokens, parts, dictionary keys, the JS API) is public and versioned (ADR-0006), so it must stay small and semantic.
    • Inspirations set the mood only. The designs are original.