Files
shiftcraft/.claude/skills/nuxt-ui/references/theming.md
2026-04-17 23:26:01 +00:00

11 KiB
Raw Blame History

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

  1. 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;
}
  1. 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 (50950) ... */
  --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) 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)