Theming
Understand the theming system and how to customize design tokens.
The exaBase Design System uses CSS custom properties (CSS variables) as design tokens, mapped to Tailwind CSS utility classes via Tailwind v4's @theme inline directive. Light and dark themes are defined in :root and .dark selectors in your globals.css.
For dark mode setup, see Dark Mode.
How It Works
The theming system has two layers:
Layer 1: CSS Custom Properties
Semantic color tokens are defined as CSS variables in :root (light) and .dark (dark):
:root {
--primary: rgb(37, 99, 244);
--primary-foreground: rgb(255, 255, 255);
}
.dark {
--primary: rgb(37, 99, 244);
--primary-foreground: rgb(255, 255, 255);
}Layer 2: Tailwind @theme inline
The @theme inline block maps these CSS variables to Tailwind's --color-* namespace, making them available as utility classes:
@theme inline {
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
/* ... */
}This allows you to use bg-primary, text-primary-foreground, and other utility classes in your components:
<Button className="bg-primary text-primary-foreground">Click me</Button>The original shadcn/ui uses oklch notation for all colors. exaBase uses rgb / rgba for core semantic tokens, and oklch for chart and sidebar tokens.
Naming Conventions
background and foreground
Colors follow a background / foreground pair convention. The background variable is used for the background color of the component and the foreground variable is used for the text color on that background.
--background: rgb(255, 255, 255); /* body background */
--foreground: rgb(8, 10, 12); /* body text */The background suffix is omitted when naming a component's background color:
--primary: rgb(37, 99, 244); /* background suffix omitted */
--primary-foreground: rgb(255, 255, 255); /* text on primary background */hovered and pressed
Interactive colors have hovered and pressed variants for mouse-over and mouse-down states:
--primary-hovered: rgb(58, 114, 245); /* + White 10% */
--primary-pressed: rgb(81, 130, 246); /* + White 20% */text
The text suffix is used for text displayed on the default --background, while foreground is for text on its counterpart component background:
--background: rgb(255, 255, 255);
--info: rgb(0, 177, 242);
--info-foreground: rgb(255, 255, 255); /* text on --info background */
--info-text: rgb(0, 152, 208); /* text on --background */muted
The muted suffix is a softened tone variant, used for subtle backgrounds in components like Alert:
--destructive-muted: rgb(254, 227, 226);
--info-muted: rgb(224, 246, 253);Opacity Modifier Syntax
Most colors support Tailwind's opacity modifier syntax:
<div className="bg-primary/50">...</div>However, colors with a pre-applied alpha channel do not support this syntax:
--secondary: rgba(103, 120, 145, 0.1);
--secondary-hovered: rgba(103, 120, 145, 0.2);
--secondary-pressed: rgba(103, 120, 145, 0.3);
--accent: var(--secondary);
--accent-pressed: var(--secondary-hovered);// Not supported
<div className="bg-secondary/50">...</div>Color Tokens
Base
| Token | Light | Dark | Description |
|---|---|---|---|
background | Default background (<body> etc.) | ||
foreground | Default text color |
Surface
| Token | Light | Dark | Description |
|---|---|---|---|
card | Background for <Card> | ||
card-foreground | Text on card | ||
popover | Background for popovers and dropdowns | ||
popover-foreground | Text on popover |
Variant
| Token | Light | Dark | Description |
|---|---|---|---|
primary | Primary color | ||
primary-hovered | Primary hover state | ||
primary-pressed | Primary pressed state | ||
primary-foreground | Text on primary | ||
primary-text | Primary text on background | ||
secondary | Secondary color | ||
secondary-hovered | Secondary hover state | ||
secondary-pressed | Secondary pressed state | ||
secondary-foreground | Text on secondary | ||
destructive | Destructive / error color | ||
destructive-hovered | Destructive hover state | ||
destructive-pressed | Destructive pressed state | ||
destructive-foreground | Text on destructive | ||
destructive-text | Destructive text on background | ||
destructive-muted | Destructive muted background | ||
info | Info color | ||
info-foreground | Text on info | ||
info-text | Info text on background | ||
info-muted | Info muted background | ||
success | Success color | ||
success-foreground | Text on success | ||
success-text | Success text on background | ||
success-muted | Success muted background | ||
warning | Warning color | ||
warning-foreground | Text on warning | ||
warning-text | Warning text on background | ||
warning-muted | Warning muted background |
Utility
| Token | Light | Dark | Description |
|---|---|---|---|
muted | Muted background | ||
muted-foreground | Muted text | ||
accent | Accent for hover effects (ghost buttons) | ||
accent-pressed | Accent pressed state | ||
accent-foreground | Text on accent | ||
border | Default border color | ||
input | Border color for inputs | ||
ring | Focus ring color |
Sidebar
| Token | Light | Dark | Description |
|---|---|---|---|
sidebar | Sidebar background | ||
sidebar-foreground | Sidebar text | ||
sidebar-primary | Sidebar primary | ||
sidebar-primary-foreground | Sidebar primary text | ||
sidebar-accent | Sidebar accent | ||
sidebar-accent-foreground | Sidebar accent text | ||
sidebar-border | Sidebar border | ||
sidebar-ring | Sidebar focus ring |
Typography
The default font stack is configured via CSS variables and mapped to font-sans:
@theme inline {
--font-sans: var(--font-inter), var(--font-noto-sans-jp), sans-serif;
}The font variables --font-inter and --font-noto-sans-jp are set by the fonts component. See Installation for setup instructions.
Border Radius
A base --radius variable controls all border radius sizes. Each size is derived from this base value:
:root {
--radius: 0.625rem;
}
@theme inline {
--radius-xs: calc(var(--radius) - 6px); /* 4px */
--radius-sm: calc(var(--radius) - 4px); /* 6px */
--radius-md: calc(var(--radius) - 2px); /* 8px */
--radius-lg: var(--radius); /* 10px */
--radius-xl: calc(var(--radius) + 4px); /* 14px */
--radius-2xl: calc(var(--radius) + 8px); /* 18px */
--radius-3xl: calc(var(--radius) + 12px); /* 22px */
--radius-4xl: calc(var(--radius) + 16px); /* 26px */
}These values differ from Tailwind CSS defaults. See Rounded for a visual reference.
Customizing the Theme
Changing Colors
Override any color by modifying the CSS variable in globals.css. No JavaScript configuration is needed:
:root {
--primary: rgb(99, 102, 241); /* your custom primary */
--primary-hovered: rgb(129, 140, 248);
--primary-pressed: rgb(165, 180, 252);
}
.dark {
--primary: rgb(129, 140, 248); /* your custom dark primary */
--primary-hovered: rgb(165, 180, 252);
--primary-pressed: rgb(199, 210, 254);
}Adding Custom Colors
To add a new color token, define the CSS variable and register it in @theme inline:
@theme inline {
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
}
:root {
--brand: rgb(255, 107, 53);
--brand-foreground: rgb(255, 255, 255);
}
.dark {
--brand: rgb(255, 140, 100);
--brand-foreground: rgb(255, 255, 255);
}Now bg-brand, text-brand-foreground, and other utility classes are available.