11 KiB
Theming
Semantic colors
| Color | Default | Purpose |
|---|---|---|
primary |
green | CTAs, active states, brand |
secondary |
blue | Secondary actions |
success |
green | Success messages |
info |
blue | Informational |
warning |
yellow | Warnings |
error |
red | Errors, destructive actions |
neutral |
slate | Text, borders, disabled |
Configuring colors
// Nuxt — app.config.ts
export default defineAppConfig({
ui: {
colors: {
primary: 'indigo',
secondary: 'violet',
success: 'emerald',
error: 'rose',
neutral: 'zinc'
}
}
})
// Vue — vite.config.ts
ui({
ui: {
colors: { primary: 'indigo', secondary: 'violet', neutral: 'zinc' }
}
})
You can only use colors that exist in your theme — either Tailwind's default colors or custom colors defined with @theme.
Adding custom colors
- Define all 11 shades in CSS:
/* assets/css/main.css */
@theme static {
--color-brand-50: #fef2f2;
--color-brand-100: #fee2e2;
--color-brand-200: #fecaca;
--color-brand-300: #fca5a5;
--color-brand-400: #f87171;
--color-brand-500: #ef4444;
--color-brand-600: #dc2626;
--color-brand-700: #b91c1c;
--color-brand-800: #991b1b;
--color-brand-900: #7f1d1d;
--color-brand-950: #450a0a;
}
- Assign it as a semantic color value:
ui: { colors: { primary: 'brand' } }
You can only use colors that have all shades defined — either from Tailwind's defaults or custom @theme definitions.
Extending with new semantic color names
If you need a new semantic color beyond the defaults (e.g., tertiary), register it in theme.colors:
// Nuxt — nuxt.config.ts
export default defineNuxtConfig({
ui: {
theme: {
colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error']
}
}
})
// Vue — vite.config.ts
ui({
theme: {
colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error']
}
})
Then assign it: ui: { colors: { tertiary: 'indigo' } } and use it via the color prop: <UButton color="tertiary">.
CSS utilities
Text
| Class | Use | Light value | Dark value |
|---|---|---|---|
text-default |
Body text | neutral-700 |
neutral-200 |
text-muted |
Secondary text | neutral-500 |
neutral-400 |
text-dimmed |
Placeholders, hints | neutral-400 |
neutral-500 |
text-toned |
Subtitles | neutral-600 |
neutral-300 |
text-highlighted |
Headings, emphasis | neutral-900 |
white |
text-inverted |
On dark/light backgrounds | white |
neutral-900 |
Background
| Class | Use | Light value | Dark value |
|---|---|---|---|
bg-default |
Page background | white |
neutral-900 |
bg-muted |
Subtle sections | neutral-50 |
neutral-800 |
bg-elevated |
Cards, modals | neutral-100 |
neutral-800 |
bg-accented |
Hover states | neutral-200 |
neutral-700 |
bg-inverted |
Inverted sections | neutral-900 |
white |
Border
| Class | Use | Light value | Dark value |
|---|---|---|---|
border-default |
Default borders | neutral-200 |
neutral-800 |
border-muted |
Subtle borders | neutral-200 |
neutral-700 |
border-accented |
Emphasized borders | neutral-300 |
neutral-700 |
border-inverted |
Inverted borders | neutral-900 |
white |
Semantic color utilities
Each semantic color (primary, secondary, success, info, warning, error) is available as a Tailwind utility: text-primary, bg-primary, border-primary, ring-primary, etc.
They resolve to shade 500 in light mode and shade 400 in dark mode (via --ui-<color> CSS variables). This is generated at runtime by the colors plugin — you don't need to write dark-mode variants manually.
To adjust which shade is used, override --ui-primary (or any semantic color) in your main.css:
:root { --ui-primary: var(--ui-color-primary-600); }
.dark { --ui-primary: var(--ui-color-primary-300); }
CSS variables
All customizable in main.css:
:root {
--ui-radius: 0.25rem; /* base radius for all components */
--ui-container: 80rem; /* UContainer max-width */
--ui-header-height: 4rem; /* UHeader height */
--ui-primary: var(--ui-color-primary-500); /* adjust shade used */
}
.dark {
--ui-primary: var(--ui-color-primary-400);
}
Solid colors (black/white)
:root { --ui-primary: black; }
.dark { --ui-primary: white; }
Component theme customization
How it works
Components are styled with Tailwind Variants. The theme defines:
slots— named style targets (e.g.,root,base,label,leadingIcon)variants— styles applied based on props (e.g.,color,variant,size)compoundVariants— styles for specific prop combinations (e.g.,color: 'primary'+variant: 'outline')defaultVariants— default prop values when none are specified
Override priority
ui prop / class prop > global config > theme defaults
The ui prop overrides slots after variants are computed. If the size: 'md' variant applies size-5 to trailingIcon, and you set :ui="{ trailingIcon: 'size-3' }", the size-3 wins.
Tailwind Variants uses tailwind-merge under the hood so conflicting classes are resolved automatically.
Understanding the generated theme
Every component's full resolved theme is generated at build time. Always read this file before customizing a component — it shows exactly what classes are applied where.
- Nuxt:
.nuxt/ui/<component>.ts - Vue:
node_modules/.nuxt-ui/ui/<component>.ts
For example, the card theme:
{
slots: {
root: "rounded-lg overflow-hidden",
header: "p-4 sm:px-6",
body: "p-4 sm:p-6",
footer: "p-4 sm:px-6"
},
variants: {
variant: {
outline: { root: "bg-default ring ring-default divide-y divide-default" },
soft: { root: "bg-elevated/50 divide-y divide-default" }
}
},
defaultVariants: { variant: "outline" }
}
Global config
Override the theme for all instances of a component:
// Nuxt — app.config.ts
export default defineAppConfig({
ui: {
button: {
slots: {
base: 'font-bold rounded-full'
},
variants: {
size: {
md: { leadingIcon: 'size-4' }
}
},
compoundVariants: [{
color: 'neutral',
variant: 'outline',
class: { base: 'ring-2' }
}],
defaultVariants: {
color: 'neutral',
variant: 'outline'
}
}
}
})
// Vue — vite.config.ts
ui({
ui: {
button: {
slots: { base: 'font-bold rounded-full' },
defaultVariants: { color: 'neutral', variant: 'outline' }
}
}
})
Per-instance (ui prop)
Overrides slots after variant computation:
<UButton :ui="{ base: 'font-mono', trailingIcon: 'size-3 rotate-90' }" />
<UCard :ui="{ root: 'shadow-xl', body: 'p-8' }" />
Per-instance (class prop)
Overrides the root or base slot:
<UButton class="rounded-none">Square</UButton>
Components without slots (e.g., UContainer, USkeleton, UMain) only have the class prop.
Theme structure patterns
Slots-based (most components — slots is an object in the generated theme):
// global config
ui: {
button: {
slots: { base: 'font-bold' }
}
}
// per instance
<UButton :ui="{ base: 'font-bold' }" />
Flat base (base is a top-level string in the generated theme):
// global config
ui: {
container: {
base: 'max-w-lg'
}
}
// per instance — class prop only
<UContainer class="max-w-lg" />
Always check the generated theme file to see which pattern applies.
Dark mode
const colorMode = useColorMode()
colorMode.preference = 'dark' // 'light', 'dark', 'system'
<UColorModeButton /> <!-- Toggle -->
<UColorModeSelect /> <!-- Dropdown -->
Fonts
/* assets/css/main.css */
@theme {
--font-sans: 'Public Sans', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
In Nuxt, fonts defined with @theme are automatically loaded by the @nuxt/fonts module.
Brand customization playbook
Follow these steps to fully rebrand Nuxt UI (e.g., "make a Ghibli theme", "match our corporate brand"):
Step 1 — Define the color palette
Pick colors that match the brand. Map them to semantic roles:
// app.config.ts (Nuxt) or vite.config.ts (Vue)
ui: {
colors: {
primary: 'emerald', // brand accent
secondary: 'amber', // secondary accent
success: 'green',
info: 'sky',
warning: 'orange',
error: 'rose',
neutral: 'stone' // affects all text, borders, backgrounds
}
}
If no Tailwind default color fits, define custom shades in CSS (see Adding custom colors):
@theme static {
--color-forest-50: #f0fdf4;
/* ... all 11 shades (50–950) ... */
--color-forest-950: #052e16;
}
Then use it: primary: 'forest'.
Step 2 — Set fonts
/* assets/css/main.css */
@theme {
--font-sans: 'Quicksand', system-ui, sans-serif;
}
Step 3 — Adjust CSS variables
:root {
--ui-radius: 0.75rem; /* rounder = softer/playful, smaller = sharper/corporate */
--ui-primary: var(--ui-color-primary-600); /* adjust which shade is used */
}
.dark {
--ui-primary: var(--ui-color-primary-400);
}
Step 4 — Override key components globally
Read the generated theme files to find slot names, then apply global overrides:
// app.config.ts (Nuxt) or vite.config.ts (Vue)
ui: {
// ... colors from Step 1
button: {
slots: {
base: 'rounded-full font-semibold'
},
defaultVariants: {
variant: 'soft'
}
},
card: {
slots: {
root: 'rounded-2xl shadow-lg'
}
},
badge: {
slots: {
base: 'rounded-full'
}
}
}
Tip
: Read
.nuxt/ui/button.ts(Nuxt) ornode_modules/.nuxt-ui/ui/button.ts(Vue) to see all available slots and variants before overriding.
Step 5 — Verify dark mode
Check that both modes look correct. Adjust --ui-primary shade per mode and test contrast. Use useColorMode() to toggle during development.
Quick checklist
| Step | What to change | Where |
|---|---|---|
| Colors | primary, secondary, neutral |
app.config.ts / vite.config.ts |
| Custom palette | 11 shades per color | main.css (@theme static) |
| Fonts | --font-sans, --font-mono |
main.css (@theme) |
| Radius | --ui-radius |
main.css (:root) |
| Primary shade | --ui-primary |
main.css (:root + .dark) |
| Component shapes | Global slot overrides | app.config.ts / vite.config.ts |
| Dark mode | Verify contrast, adjust variables | main.css (.dark) |