Theming

Colors

Semantic colors, the three CSS-variable tiers, and how to configure or add a color.

@vyui/kit ships six configurable semantic colors (primary, secondary, success, info, warning, error) plus neutral, which is always present but kept out of the configurable list (mirrors nuxt/ui, which excludes neutral from theme.colors too). Source of truth: packages/kit/src/theme/color-constants.js.

Default semantic → Tailwind-palette mapping:

SemanticPalette
primarygreen
secondaryblue
successemerald
infosky
warningamber
errorred
neutralslate

Not the obvious 1:1 (success isn't stock green, info isn't stock blue): Tailwind's base yellow / blue / green read too washed out at shade-500 for contrast on white, so amber / sky / emerald stand in.

Three CSS-variable tiers

packages/kit/src/style.css defines every color as three tiers of custom property. This is the mental model to keep in your head when overriding or reading theme files:

  1. Palette covers --ui-color-{semantic}-{50..950}, the full Tailwind shade scale per semantic color (e.g. --ui-color-primary-500: theme('colors.green.500')). Powers ramp utilities: bg-primary-500, border-error-200.
  2. Mode covers --ui-{semantic} (e.g. --ui-primary), a single active shade per color mode (-500 in light, -400 in dark). Powers shorthand utilities: bg-primary, text-error.
  3. Semantic / surface holds role-based tokens about UI role, not a specific color, that flip entirely between light and dark: text-dimmed/muted/toned/default/highlighted/inverted, bg-default/muted/elevated/accented/inverted, border-default/muted/accented/inverted. Component themes use these for text, surfaces, and borders instead of raw neutral-* classes, so components adapt to dark mode automatically with zero dark: variants.

One nuance that trips people up: the palette tier's neutral ramp does not invert. bg-neutral-100 is the literal same slate shade in light or dark mode, because picking a raw ramp class is a "you picked a literal shade" contract. Only the semantic/surface tier flips. Reach for bg-muted, not bg-neutral-100, for anything that should adapt to dark mode.

Configuring colors

Two ways to customize, depending on how far you need to go.

Rebrand via CSS only by overriding the --ui-color-{semantic}-{shade} variables in your own stylesheet, after importing @vyui/kit/style.css. No build step, no config file:

src/index.css
@import '@vyui/kit/style.css';

:root {
  --ui-color-primary-500: #6366f1;
  --ui-color-primary-600: #4f46e5;
}

This only rebrands a color already in the configurable set; it doesn't add a new slot.

Add a new color slot such as tertiary by noting that the Tailwind preset has to generate the classes while the runtime has to select the same set. Author the color list once and feed it to both planes:

vyui.config.ts
import type { VyUIPluginOptions } from '@vyui/kit'
import { COLORS } from '@vyui/kit/tailwind'

export default {
  ui: { colors: [...COLORS, 'tertiary'] },
} satisfies VyUIPluginOptions
tailwind.config.ts
import vyuiConfig from './vyui.config'
import { createVyuiPreset } from '@vyui/kit/tailwind'

export default {
  presets: [lynxPreset, createVyuiPreset(vyuiConfig)],
}
src/index.ts
import vyuiConfig from './vyui.config'
import { provideVyUI } from '@vyui/kit' // or: app.use(VyUI, vyuiConfig) on web

provideVyUI(app, vyuiConfig)

createVyuiPreset dev-warns if a listed color has no backing --ui-color-* var, so a "class resolves to nothing" mistake surfaces at build time instead of on device. To also type-check tertiary on every component's color prop:

vyui-colors.d.ts
declare module '@vyui/kit' {
  interface VyuiColorRegistry { tertiary: true }
}

Then add the --ui-color-tertiary-{50..950} CSS-var block, generating the .d.ts and the CSS together with:

node node_modules/@vyui/kit/scripts/gen-colors.mjs --colors tertiary=indigo

See the @vyui/kit README for the full flow, or Installation for how the config object fits alongside the rest of the plugin setup.