Theming
Customize the docs shell and published components with your own design tokens.
The docs app uses the local shadcn-vue configuration in components.json and the global theme in
registry/items/files/styles/globals.css.
Published registry items are independent from the docs shell. Put installable component source in
registry/items/**, and include any required CSS, dependencies, or registry dependencies in the
item's _registry.md frontmatter.
Use semantic tokens such as background, foreground, primary, muted, and border so
installed components remain easy to customize.
How light/dark switching works
theme.css defines every token twice: once under :root (light) and once under .dark (dark).
globals.css imports theme.css and declares @custom-variant dark (&:is(.dark *));, so any
dark: utility a component uses resolves against the values overridden inside a .dark ancestor.
Neither file toggles that class on its own — something in the consuming app has to add or remove
dark on <html> (or another ancestor) at runtime.
In this docs app that "something" is @nuxtjs/color-mode,
configured in nuxt.config.ts:
colorMode: {
classSuffix: "",
},
The module's default behavior suffixes its class (dark-mode / light-mode); classSuffix: ""
makes it emit plain dark / light instead, so it lines up with the bare .dark selector
theme.css expects. app/components/docs/ThemeToggle.vue — part of the docs shell, not a
published registry item — flips the mode:
const colorMode = useColorMode();
function toggleTheme() {
colorMode.preference = colorMode.value === "dark" ? "light" : "dark";
}
Wiring it up in a consumer project
Installing the styles item only gives you the CSS tokens — it does not install a toggle
component or a color-mode module. To make dark: utilities respond to anything, add your own
mechanism that toggles a dark class:
- Nuxt: add
@nuxtjs/color-modeand setclassSuffix: ""to matchtheme.css's bare.darkselector, the same way this project does. - Other Vue setups: VueUse's
useColorModecomposable applies the same class-toggling pattern without Nuxt. - Manual: toggle it yourself, e.g.
document.documentElement.classList.toggle("dark"), and persist the preference (localStorage, a cookie, etc.) however you like.
Whatever you choose, the class name must be exactly dark — theme.css doesn't know about any
other naming convention.
Accent theme variants
registry/items/files/styles/ also has theme-secondary.css and theme-tertiary.css. Each scopes
an accent palette under [data-theme="theme-secondary"] / [data-theme="theme-tertiary"] (plus a
.dark[data-theme="..."] variant for dark mode), overriding just the accent-related tokens
(primary, ring, sidebar-primary, selection, chart-*) — everything else still comes from
theme.css's :root / .dark. globals.css imports both files (after theme.css, so their
attribute selectors win the cascade), and the styles item publishes them alongside globals.css
and theme.css.
Like dark: utilities, nothing applies automatically — some ancestor needs a data-theme attribute
set at runtime. This docs app's Palette icon button in the header (AccentThemeToggle.vue) does
that via useAccentTheme() (app/composables/useAccentTheme.ts), which mirrors the light/dark
pattern:
function applyAccentTheme(value: AccentTheme) {
if (value === "default") {
document.documentElement.removeAttribute("data-theme");
} else {
document.documentElement.setAttribute("data-theme", value);
}
}
The preference persists via useStorage under the accent-theme key, and a small blocking script
in app.vue reads that key and sets data-theme before first paint — the same anti-flash trick
@nuxtjs/color-mode uses for dark/light, hand-rolled here since there's no module for it.
A consumer project can reuse the same shape: read/write a stored preference and toggle a
data-theme attribute to "theme-secondary" / "theme-tertiary" (or remove it for the default
palette) on whatever element should be re-themed.