The public theming contract is CSS variables. The theme pipeline starts with seed tokens and ends in component-level tokens.

seed tokens -> OKLCH palettes -> semantic tokens -> component tokens

Use provideKikitaUi() for the default theme and optional root configuration.

import { type ApplicationConfig } from '@angular/core';
import { provideKikitaUi } from '@kikita-labs/ui';

export const appConfig: ApplicationConfig = {
  providers: [provideKikitaUi()],
};

Where you set a token decides how far it reaches. Components read semantic or component tokens, never palette or seed variables.

NameTypeDefaultDescription
A seed in provideKikitaUi({ theme })whole theme — Palette, semantic roles and literal component tokens are regenerated for every element.
A semantic token on any elementelement and below — --kui-color-danger-fill on an element restyles that element and everything below it; on the root it changes the whole page.
A component token on any elementelement and below — --kui-btn-danger-bg restyles that one component part for the element and everything below it.
A palette step or a type role aliasset globally — Not reliable on an element: semantic tokens and --kui-type-* are resolved from them on the root, so set them there.
billing.css
.billing-panel {
  --kui-btn-danger-bg: oklch(0.5 0.2 25);
  --kui-btn-danger-bg-hov: oklch(0.44 0.2 25);
}
  • Component tokens are inputs, not outputs. A component states its default in its own CSS, for example var(--kui-card-bg, var(--kui-color-surface)), and the generated stylesheet does not define that token. Setting --kui-card-bg on an ancestor therefore wins, and so does changing --kui-color-surface on the same ancestor.
  • The generated stylesheet defines only literal component tokens (sizes, gaps, durations), the semantic --kui-type-* roles and the density-dependent --kui-btn-px. Do not read a component token in your own CSS without a fallback: it has no value unless someone sets it.
  • Class names are a separate namespace: a kui-* class is stable only where a component page documents it, and the other anatomy classes are internal and may change in a minor release.

The default theme works from CSS alone, and your own variables always win over the generated ones.

  • @kikita-labs/ui/styles includes theme-default.css, generated from DEFAULT_KUI_THEME, so the first paint does not wait for a script. provideKikitaUi() adds the theme of custom seeds on top of it.
  • The generated variables live in the kui.tokens cascade layer, declared before kui.base and kui.components. A variable that you write outside any layer, for example on the root, always wins over the generated one whatever the order of the style sheets.
  • provideKikitaUi() sets data-kui-density on the root element from seeds.density, on the server too, unless the page already set it.
  • The neutral seed tints the neutral scales of both modes, so surfaces, borders, text, skeletons and scrollbars follow seeds.neutral. For any seed every pair of roles the library draws reaches 4.5:1 (text) or 3:1 (non-text); the generator picks white or near-black text for each solid fill and corrects a seed that neither reaches 4.5:1 on.

WCAG 1.4.11 asks for 3:1 on the boundary of a control only where that boundary identifies it. A contrast profile re-points the neutral roles that draw it.

import { DEFAULT_KUI_THEME, provideKuiTheme } from '@kikita-labs/ui';

provideKuiTheme({ ...DEFAULT_KUI_THEME, contrast: 'soft' });
NameTypeDefaultDescription
KuiThemeContrast'strict' | 'soft' — Contrast profile name and the value of the data-kui-contrast attribute.
DEFAULT_KUI_THEME_CONTRASTKuiThemeContrast — Profile a theme uses when the contrast option is not set.
KuiGeneratedContrast{ name; media?; light; dark } — Variables one profile changes relative to the default, emitted as [data-kui-contrast] rules and an optional media query.
  • strict (default): control borders (--kui-color-border-control) reach 3:1 on every surface. soft: they read quieter (neutral 6, hover 8) and fall below 3:1, so choose it knowingly. Text, focus rings and fills do not change.
  • The option sets the default. The other profile is emitted as the variables it changes, so data-kui-contrast switches it at runtime without a new theme.
  • strict also applies by itself under prefers-contrast: more while the attribute is absent, so a visitor who asked for more contrast still gets 3:1 borders with a soft default. An explicit attribute always wins over the system setting.
  • This documentation site runs with soft by default for a quieter look and lets you pick Strict or Soft in the theming palette. The choice is stored only after you make it, so the operating system setting keeps working until then.
  • Keep profiles to border and surface roles: body text stays at 4.5:1 in every profile.

A few tokens change one behaviour for the whole library. Set them on the root or on any subtree.

styles.css
:root {
  --kui-opacity-disabled: 0.4; /* every disabled control, option and cell */
  --kui-focus-ring-width: 2px; /* standalone controls; parts use --kui-focus-ring-width-sm */
  --kui-duration-base: 120ms; /* enter animations and control state changes */
  --kui-color-scrim: oklch(0 0 0 / 0.65); /* behind Dialog, Drawer and Command Palette */
}

A component hook such as --kui-btn-focus-ring-w no longer exists; the shared token is the one to set. The full list is on the Tokens page.

The 2.x colour system keeps every public token name, so an existing theme keeps working. Move to the new roles when you next touch a custom component.

NameTypeDefaultDescription
--kui-color-on-fill on a solid fill--kui-color-<role>-on-fill — White or near-black, whichever reaches 4.5:1 on that fill.
--kui-color-<role>-fill as text or an icon colour--kui-color-<role>-text — 4.5:1 on every surface.
--kui-color-<role>-fill as a border, outline, bar or mark--kui-color-<role>-indicator — 3:1 on every light surface.
--kui-color-border on an interactive control--kui-color-border-control — A 3:1 boundary at rest; the hover value is --kui-color-border-control-hover.
--kui-color-primary-focus-ring or a box-shadow ringa 2px outline in --kui-color-focus — Visible in forced-colors mode and 3:1.
--kui-color-text-disabled on placeholders--kui-color-text-placeholder — 4.5:1; disabled text stays for disabled controls.
  • --kui-neutral-1 to -12 are one scale per mode, accent steps sit at fixed tones and --kui-input-focus-ring defaults to none.
  • --kui-color-on-fill and --kui-color-primary-focus-ring are deprecated and removed in 3.0.
  • Under Windows High Contrast, forced-colors.css states checked controls, Slider, Progress, selected cells, Tabs, Segmented, pressed Chip, Stepper and the charts with system colours.

Seed values are the stable way to customize generated palettes and shared scales.

app.config.ts
provideKikitaUi({
  theme: {
    seeds: {
      color: {
        primary: 'oklch(0.52 0.25 285)',
        neutral: 'oklch(0.5 0.01 80)',
        success: 'oklch(0.54 0.16 145)',
        warning: 'oklch(0.74 0.16 75)',
        danger: 'oklch(0.54 0.22 25)',
        info: 'oklch(0.58 0.16 215)',
      },
      radius: 8,
      density: 'regular',
    },
  },
});

These public functions can generate CSS variables for docs, visual tests, or non-Angular tooling.

NameTypeDefaultDescription
createKuiTheme(options?: KuiThemeOptions) => KuiGeneratedTheme — Generates a theme object from seed options.
createKuiThemeVariableMap(theme, mode?) => KuiCssVariableMap — Flattens a generated theme into CSS variable names and values.
createKuiThemeCssText(theme, selector, mode?) => string — Serializes variables for a selector.
createKuiThemeStyleSheet(theme) => string — Serializes light and dark theme CSS for tooling or docs output.