Skip to content
kitn AI/UI

Theming

Set the --kai-color-* tokens on :root to rebrand every kai-* web component at once. Inherited custom properties pierce the Shadow DOM boundary, so a single CSS rule propagates into all of them with no stylesheet import.

The kit’s appearance is driven entirely by CSS custom properties defined in theme.css. Each token follows a semantic naming convention borrowed from shadcn/ui — names describe purpose (--color-primary), not hue (--color-blue) — and surface/text pairs come together:

Token pairControls
--color-background / --color-foregroundPage/chat surface and text
--color-primary / --color-primary-foregroundButtons, active states, accents
--color-secondary / --color-secondary-foregroundSecondary buttons, chips
--color-muted / --color-muted-foregroundSubdued surfaces and placeholder text
--color-card / --color-card-foregroundMessage bubbles, panels
--color-borderDividers and input outlines
--color-inputInput field background
--color-destructive / --color-destructive-foregroundError and danger states
--color-ringFocus ring (blue by default for WCAG AA contrast)
--color-sidebarConversation sidebar background
--color-code-foregroundInline code text and chip accent

Border radii are controlled by --radius (default 0.6rem); --radius-sm, --radius-md, --radius-lg, --radius-xl, --radius-2xl and --radius-3xl derive from it, plus --radius-pill for the pill family (badges, chips, switch tracks).

Those are Tailwind’s own radius names, so in a Tailwind build that imports theme.css they also become what rounded-sm through rounded-3xl and rounded-pill mean. See what theme.css changes.

The kit injects its own scoped CSS into each component’s Shadow DOM. The kai-* web components need no import at all, and the lightest way to retheme them is the --kai-color-* tokens with no stylesheet.

Two files ship for when you want the same --color-* tokens on your own markup or the light-DOM SolidJS components. Pick by what your build does with CSS:

FileIsReach for it when
@kitn.ai/ui/theme.tokens.cssPlain CSS: a :root block, a .dark block, the kit’s keyframes and two classesYou want the token names for your own CSS and nothing touching your Tailwind config. The default for Tailwind apps too, unless you want the kit’s scale.
@kitn.ai/ui/theme.cssTailwind v4 source: an @theme block, a @custom-variant, @utility rulesYou run Tailwind and want to opt in to the kit’s scale wholesale: its colors as your utilities, its radius and type scale, class-based dark mode.
import '@kitn.ai/ui/theme.tokens.css'; // plain CSS, any bundler
import '@kitn.ai/ui/theme.css'; // Tailwind v4 source, only when Tailwind compiles it

Or via CDN:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@kitn.ai/ui/dist/theme.tokens.css">

A plain <link> can’t use theme.css: the browser discards its @theme block whole, so no tokens land. theme.tokens.css is generated from it on every build and never drifts.

What theme.css changes in a Tailwind build

Section titled “What theme.css changes in a Tailwind build”

theme.css is @imported into your Tailwind build, so it edits your theme, not only the kit’s. Everything it changes:

Declaration in theme.cssEffect on your build
@custom-variant dark (&:is(.dark *))Your dark: utilities switch from prefers-color-scheme to the .dark class. Until something adds .dark to an ancestor, every dark: rule you already have stops matching. theme.tokens.css does not do this.
--color-* in @themeMerge into your @theme under the shadcn names. The last import wins, so a --color-primary you already define is replaced unless yours comes later.
--radius-sm / -md / -lg / -xl / -2xl / -3xlRe-pointed at the kit’s --radius (0.6rem): --radius-lg: var(--radius), -sm: calc(var(--radius) - 4px), -md: calc(var(--radius) - 2px), -xl: calc(var(--radius) + 4px), -2xl: + 8px, -3xl: + 12px. Your rounded-* utilities follow, so rounded-lg is 0.6rem instead of Tailwind’s 0.5rem. The ladder is deliberately COMPLETE — a rung left at its stock value is a component that silently ignores the knob.
--radius-pillA new rung, and a new rounded-pill utility with it (--radius-pill: var(--kai-radius-pill, 4rem)). It exists because Tailwind compiles rounded-full to a literal no custom property can reach, so badges, chips, tags, switch tracks and count bubbles could not follow a consumer’s shape choice at all. Fully round by default; set it low to square the pill family along with the rest. rounded-full is untouched and still means a circle.
--code-radiusThe code surface’s corner (--code-radius: var(--kai-code-radius, 0.75rem)), read by fenced code blocks and the code panel. Its own value rather than a rung of the ladder — a code block inside a card wants a corner of its own.
--shadow / --shadow-*Every rung is Tailwind’s own geometry with its lengths MULTIPLIED by --kai-shadow-strength (unitless, default 1), which is what makes elevation one decision instead of seven: --kai-shadow-strength: 0 is a flat product. Bare shadow reads the --shadow key rather than --shadow-sm, which is why it is declared separately — it is most of the kit’s call sites. Defaults reproduce Tailwind exactly, so nothing moves unless you set it.
--font-weight-normal / -medium / -semibold / -boldRe-pointed at --kai-weight-*, defaulting to Tailwind’s own 400 / 500 / 600 / 700. One knob per rung rather than a single “weight” control: a product that wants a heavier semibold rarely wants a heavier normal too.
--text-xs / -sm / -base / -lgAliased onto the kit’s type scale. The defaults equal stock Tailwind, so nothing moves until you set a --kai-text-* token, and then your text-sm moves with it.
--spacingRe-pointed at the kit’s --kai-density, whose fallback is stock Tailwind’s 0.25rem — so nothing moves until you set it. Every numeric spacing utility is calc(var(--spacing) * N), which makes this the widest-reaching declaration in the file: your p-*, m-*, gap-*, h-*, size-*, space-y-* and the offset utilities (top-*, -mt-*, translate-*) all follow it. Set it once for a denser or airier kit.
@utility bg-surface, bg-hover, bg-selected, bg-*-softNew utilities in your build. Additive, they collide with nothing Tailwind ships.
@keyframes kai-*, .kai-scrollbar-thin, .chat-markdownGlobal names, listed below.

theme.tokens.css skips the first row and the @utility row. It still declares the same --color-*, --radius-*, --text-*, --spacing, --shadow* and --font-weight-* names on :root, and Tailwind v4 utilities read those variables at runtime, so the radius values above change under it as well.

If you want none of the kit’s scale, import neither file and theme through --kai-*.

Density is the one scale with kit-wide reach, and it costs one declaration:

:root {
--kai-density: 0.2rem; /* 20% tighter everywhere, stock Tailwind is 0.25rem */
}

That moves padding, gaps, control heights, list rhythm and micro-offsets together, because they share the variable. It also moves size-*, so icon sizes follow the same scale — worth a look at your own icons before shipping a much denser theme.

Import whichever file you choose before your own @theme and token overrides so yours win.

Both files put these in your page’s global scope. Every one is prefixed except .chat-markdown, which is public and stays as it is.

NameWhat it is
@keyframes kai-typing, @keyframes kai-blink, @keyframes kai-shimmer, @keyframes kai-spinner-fade, and the restThe kit’s animations. Every keyframe name starts with kai-.
.kai-scrollbar-thinThin scrollbar styling used by the scroll regions in the SolidJS components
.chat-markdownThe markdown body: headings, lists, code, tables, links. Style it to restyle rendered markdown in the light-DOM components.

Shadow DOM stops selectors, not inheritance or units.

  • rem is the page’s. Every size in the kit is in rem, and rem resolves against your <html> font-size inside a shadow root too. A page that sets html { font-size: 62.5% } renders the kit at 62.5% of its intended size. Undo it on the web components you mount, or set --kai-text-* in px.
  • Inherited text properties leak in. Each element pins font-family, font-size, line-height and letter-spacing on its host. Other inherited properties, text-align, text-transform, font-weight and white-space among them, come through from whatever wraps the element. A centered or uppercased container centers or uppercases the chat. Reset them on the element (kai-chat { text-align: start; text-transform: none; }) when the wrapper’s styling is not meant for it.

Set your overrides on :root after importing theme.css. This is the host-page layer — your own markup and the light-DOM Solid components. To retheme the kai-* web components, use the --kai-color-* form instead; every token below has one.

:root {
--color-background: hsl(0 0% 6%);
--color-foreground: hsl(0 0% 96%);
--color-primary: hsl(271 91% 65%);
--color-primary-foreground: hsl(0 0% 100%);
--color-muted: hsl(0 0% 12%);
--color-muted-foreground: hsl(0 0% 55%);
--color-border: hsl(0 0% 16%);
--color-card: hsl(0 0% 10%);
--color-card-foreground: hsl(0 0% 96%);
--radius: 0.5rem;
}

Every kai-* web component carries a theme attribute — 'light' | 'dark' | 'auto', defaulting to auto. On auto the element watches prefers-color-scheme itself and applies the kit’s dark tokens inside its own shadow root, so OS dark mode works with no wiring:

<kai-chat></kai-chat> <!-- follows the OS -->
<kai-chat theme="dark"></kai-chat> <!-- pinned dark -->

For your own markup and the light-DOM Solid components, toggle the dark class on <html> (or any ancestor):

document.documentElement.classList.toggle('dark');

That class does not reach the web components. If your app owns dark mode through .dark, set theme="dark" on the web components alongside it, or they keep following the OS while the page around them goes dark.

The .dark selector in theme.css redefines every --color-* token for dark surfaces. Override inside .dark to customize the dark-mode palette:

:root {
--color-primary: hsl(271 91% 65%); /* light mode */
}
.dark {
--color-primary: hsl(271 91% 75%); /* dark mode — brighter for contrast */
}

Every --color-* token resolves through a --kai-color-* alias before falling back to its default. Override the --kai-color-* form to rebrand without importing or redefining the entire theme.css token set:

:root {
--kai-color-primary: hsl(271 91% 65%);
--kai-color-primary-foreground: hsl(0 0% 100%);
--kai-radius: 0.375rem;
}

This is the lightest-weight override path — no stylesheet import needed.

One scale runs the whole kit. Seven namespaced tokens set it, and every text surface in every web component resolves through them:

TokenDefaultTailwind twinApplies to
--kai-text-micro0.625rem—Badges, pills, uppercase eyebrows
--kai-text-caption0.6875rem—Micro labels, sub-counts, xs code
--kai-text-meta0.75remtext-xsControls, toggles, switchers, captions
--kai-text-compact0.8125rem—Dense chrome and code: a notch below body
--kai-text-body0.875remtext-smPrimary reading text, most chrome
--kai-text-title1remtext-baseEmphasis, headers
--kai-text-lg1.125remtext-lgSection headings, proseSize="lg"
:root {
--kai-text-body: 0.9375rem; /* 14px -> 15px across every web component */
}

The Tailwind twin column is what makes that global. Tailwind v4 generates text-xs / text-sm / text-base / text-lg from --text-* theme variables, and theme.css points those at the same --kai-text-* tokens. text-sm and text-body are one declaration under two names, so a token override moves every call site whichever name it spells.

Defaults match stock Tailwind exactly — nothing shifts until you set a token.

Line-heights are unitless ratios. Bump a size and the leading follows it.

Message and markdown body copy rides the same scale. proseSize picks which rung a message renders at (sm, the default, is --kai-text-body); the token sets how big that rung is. Headings, lists and code inside the markdown block are sized in em, so they follow. See Appearance settings for proseSize.

Non-color appearance is controlled by kai-chat attributes (and the matching ChatConfig props in the SolidJS layer):

SettingValuesPurpose
proseSizexs · sm · base · lgMessage / markdown text size
codeThemeany Shiki theme nameSyntax-highlight theme for code blocks
codeHighlighttrue / falseDisable code highlighting entirely (no Shiki loaded)
<kai-chat prose-size="base" code-theme="github-dark-dimmed"></kai-chat>

The full list of overridable tokens lives in theme.css (shipped at @kitn.ai/ui/theme.css). For a browser-ready version with :root and .dark blocks pre-separated, use @kitn.ai/ui/theme.tokens.css — it is auto-generated from theme.css and never drifts.