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-ringExtended 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-subtleForeground
Text hierarchy on any background.
--foreground--foreground-muted--foreground-softBorder
Three strengths for dividers and outlines.
--border--border-subtle--border-strongPrimary
Brand color plus interaction states.
--primary--primary-soft--primary-hover--primary-activeSecondary
Neutral surface with the same state ladder.
--secondary--secondary-soft--secondary-hover--secondary-activeDestructive
For destructive actions and errors.
--destructive--destructive-soft--destructive-hoverStatus
Success, warning, info. Each has a soft variant.
--success--success-soft--warning--warning-soft--info--info-softRadius
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-xsshadow-smshadowshadow-mdshadow-lgShadow with border
Paired with a border border-border utility.
shadow-xsshadow-smshadowshadow-mdshadow-lgFocus 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/20Dark 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;
}