428 lines
11 KiB
Markdown
428 lines
11 KiB
Markdown
# 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
|
||
|
||
```ts
|
||
// Nuxt — app.config.ts
|
||
export default defineAppConfig({
|
||
ui: {
|
||
colors: {
|
||
primary: 'indigo',
|
||
secondary: 'violet',
|
||
success: 'emerald',
|
||
error: 'rose',
|
||
neutral: 'zinc'
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
```ts
|
||
// 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](https://tailwindcss.com/docs/colors) or custom colors defined with `@theme`.
|
||
|
||
## Adding custom colors
|
||
|
||
1. Define all 11 shades in CSS:
|
||
|
||
```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;
|
||
}
|
||
```
|
||
|
||
2. 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`:
|
||
|
||
```ts
|
||
// Nuxt — nuxt.config.ts
|
||
export default defineNuxtConfig({
|
||
ui: {
|
||
theme: {
|
||
colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error']
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
```ts
|
||
// 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`:
|
||
|
||
```css
|
||
:root { --ui-primary: var(--ui-color-primary-600); }
|
||
.dark { --ui-primary: var(--ui-color-primary-300); }
|
||
```
|
||
|
||
### CSS variables
|
||
|
||
All customizable in `main.css`:
|
||
|
||
```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)
|
||
|
||
```css
|
||
:root { --ui-primary: black; }
|
||
.dark { --ui-primary: white; }
|
||
```
|
||
|
||
## Component theme customization
|
||
|
||
### How it works
|
||
|
||
Components are styled with [Tailwind Variants](https://www.tailwind-variants.org/). 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](https://github.com/dcastil/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:
|
||
|
||
```ts
|
||
{
|
||
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:
|
||
|
||
```ts
|
||
// 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'
|
||
}
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
```ts
|
||
// 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:
|
||
|
||
```vue
|
||
<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:
|
||
|
||
```vue
|
||
<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):
|
||
|
||
```ts
|
||
// 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):
|
||
|
||
```ts
|
||
// 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
|
||
|
||
```ts
|
||
const colorMode = useColorMode()
|
||
colorMode.preference = 'dark' // 'light', 'dark', 'system'
|
||
```
|
||
|
||
```vue
|
||
<UColorModeButton /> <!-- Toggle -->
|
||
<UColorModeSelect /> <!-- Dropdown -->
|
||
```
|
||
|
||
## Fonts
|
||
|
||
```css
|
||
/* 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:
|
||
|
||
```ts
|
||
// 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](#adding-custom-colors)):
|
||
|
||
```css
|
||
@theme static {
|
||
--color-forest-50: #f0fdf4;
|
||
/* ... all 11 shades (50–950) ... */
|
||
--color-forest-950: #052e16;
|
||
}
|
||
```
|
||
|
||
Then use it: `primary: 'forest'`.
|
||
|
||
### Step 2 — Set fonts
|
||
|
||
```css
|
||
/* assets/css/main.css */
|
||
@theme {
|
||
--font-sans: 'Quicksand', system-ui, sans-serif;
|
||
}
|
||
```
|
||
|
||
### Step 3 — Adjust CSS variables
|
||
|
||
```css
|
||
: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:
|
||
|
||
```ts
|
||
// 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) or `node_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`) |
|