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--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--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 — 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 — 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 — override 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 (e.g. tertiary) — the Tailwind preset has to generate the classes and the runtime has to select the same set, so author the color list once with defineVyuiConfig (from the light @vyui/kit/config entry — safe to import in a Tailwind config, it never pulls component code) and feed it to both planes:

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

export default defineVyuiConfig({
  theme: { colors: [...COLORS, 'tertiary'] },
})
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 — generate 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 defineVyuiConfig fits alongside the rest of the plugin setup.