Theming

Tokens, colors, radius, typography, shadows, and dark mode.

Approach

Kiwa UI uses Tailwind v4's CSS-first configuration. There is no tailwind.config.js. Every token lives in styles/globals.css inside an @theme inline block. Colors are authored in oklch, and dark mode is a single custom variant.

@import "tailwindcss";

@custom-variant dark (&:is(.dark *));

@theme inline {
  --color-background: var(--background);
  --color-primary: var(--primary);
  /* ...more tokens */
}

:root {
  --background: oklch(98.5% 0 0);
  --primary: oklch(60.9% 0.126 221.723);
  /* ...more raw values */
}

.dark {
  --background: oklch(20.5% 0 0);
  --primary: oklch(78.9% 0.154 211.53);
  /* ...dark overrides */
}

Core tokens

The base tokens that drive every component. Names follow shadcn/ui conventions so existing themes and mental models carry over.

--color-background
--color-foreground
--color-card
--color-popover
--color-primary
--color-secondary
--color-muted
--color-accent
--color-destructive
--color-border
--color-input
--color-ring

Extended tokens

Additional tokens for hover, active, soft, subtle, and raised surfaces. Components use these instead of ad-hoc opacity or brightness tricks, so interaction states stay consistent across the whole library.

Background

Three surface depths for layering UI.

--background
--background-raised
--background-subtle

Foreground

Text hierarchy on any background.

--foreground
--foreground-muted
--foreground-soft

Border

Three strengths for dividers and outlines.

--border
--border-subtle
--border-strong

Primary

Brand color plus interaction states.

--primary
--primary-soft
--primary-hover
--primary-active

Secondary

Neutral surface with the same state ladder.

--secondary
--secondary-soft
--secondary-hover
--secondary-active

Destructive

For destructive actions and errors.

--destructive
--destructive-soft
--destructive-hover

Status

Success, warning, info. Each has a soft variant.

--success
--success-soft
--warning
--warning-soft
--info
--info-soft

Radius

One base radius drives a ladder of calc()-derived sizes. Change --radius and every component updates in proportion.

--radius: 0.625rem;

--radius-xs: calc(var(--radius) - 6px);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 2px);
--radius-2xl: calc(var(--radius) + 4px);

Typography

There is one font token, --font-sans, set to InterVariable with a full stack fallback. Headings get a shared weight, tracking, and optical-size adjustment via a base @layer base rule, so you never need to apply font-* or tracking-* utilities to headings yourself.

--font-sans: 'InterVariable', 'Inter', ui-sans-serif, system-ui, sans-serif;

@layer base {
  h1, h2, h3, h4, h5, h6 {
    font-weight: 550;
    letter-spacing: -0.025em;
    font-variation-settings: 'opsz' 32;
  }
}

Shadows

Multi-layered stacked shadows with a built-in 1px edge ring and progressively doubling offsets for realistic depth. Use them alone or in combination with a border.

Shadow only

Uses the built-in 1px ring layer.

shadow-xs
shadow-sm
shadow
shadow-md
shadow-lg

Shadow with border

Paired with a border border-border utility.

shadow-xs
shadow-sm
shadow
shadow-md
shadow-lg

Focus pattern

One focus-ring pattern is used everywhere so keyboard focus looks consistent across primitives. Apply it to interactive elements you build yourself.

focus-visible:border-ring
focus-visible:ring-[3px]
focus-visible:ring-ring/20

Dark mode

Dark mode is driven by a .dark class on an ancestor element. The custom variant @custom-variant dark (&:is(.dark *)); applies dark overrides automatically to any element under it.

To add a theme toggle, see the theme behavior on the Interactivity page.

Customising

Override any token by redefining it in your own :root block after Kiwa UI's. Tokens cascade, so a single line is usually enough to re-brand the whole system.

:root {
  /* Change the brand color and every component updates */
  --primary: oklch(65% 0.2 285);
  --primary-hover: oklch(58% 0.2 285);
  --primary-active: oklch(51% 0.2 285);

  /* Tighter corners everywhere */
  --radius: 0.375rem;
}