vunor
Vunor
Vunor is two things in one package:
- A UnoCSS design-system preset (
presetVunor) that derives a complete theme — perceptual color palette, golden-ratio typography/spacing, layered backgrounds, font-aware margin correction — from a few mathematical knobs. - A Vue 3 component library of 30+ accessible components built on Reka UI, styled entirely through UnoCSS classes (zero CSS files, no scoped styles).
The two halves are independent: the preset works without the components, and the components are skinnable through the same shortcut system you'd use for your own code.
Mental model
Three layers stack on top of each other. Read top-down to understand any class:
SEMANTIC CLASSES scope-primary, layer-0, surface-100, c8-filled, i8-filled, card
↓ expand to (UnoCSS shortcuts, deep-merged from defineShortcuts() objects)
LOW-LEVEL UTILITIES bg-current, current-bg-scope-color-500, h-fingertip, p-$m
↓ resolve to (UnoCSS rules from presetVunor)
CSS CUSTOM PROPERTIES --scope-color-500, --current-bg, --v-fingertip, --card-spacing
Everything that paints, sizes, or themes flows through CSS variables. Set scope-primary once high in the tree → every descendant bg-current, c8-filled, surface-100 etc. picks up the primary palette. Change scope-error on a subtree → that subtree turns red, no other classes change.
Required foundation: scope
scope-{name} declares which palette is active for a subtree. Without an active scope, the color-aware classes have no values to render. Vunor preflights install scope-neutral on :root automatically, so things work out of the box and incidental UI (default borders, layer backgrounds, idle text/icons) reads as a calm neutral.
The recommended pattern is to keep scope-neutral as the page default and only opt into a stronger scope on accent elements:
- Brand-colored interactive elements (primary buttons, focused inputs, active tabs, brand banners) →
scope-primary(orscope-secondaryfor an alternate accent). - State-bearing elements (error inputs, destructive buttons, validation messages) →
scope-error. For warnings →scope-warn. For success →scope-good.
That way the page chrome stays neutral and the eye is drawn to the elements that genuinely need attention.
<!-- Page-level scope is neutral (preflight default — no explicit class needed) -->
<body class="layer-0">
<header>…</header>
<!-- Brand accent: only the button opts into primary -->
<button class="scope-primary c8-filled">Save</button>
<!-- State change: same input, different scope = different visual weight -->
<VuInput v-model="email" :error="emailError" />
<!-- VuInput auto-applies scope-error internally when :error is set -->
<!-- Destructive action: opt into error scope explicitly -->
<button class="scope-error c8-flat">Delete account</button>
</body>
Valid names: primary, secondary, good, warn, error, grey, neutral.
Quick start
// uno.config.ts
import { defineConfig } from 'unocss'
import { presetVunor, vunorShortcuts } from 'vunor/theme'
export default defineConfig({
presets: [presetVunor({ palette: { colors: { primary: '#6B4EFF' } } })],
shortcuts: [vunorShortcuts()],
})
// vite.config.ts — add VunorVueResolver for auto-import of <VuFoo>
import Components from 'unplugin-vue-components/vite'
import { VunorVueResolver } from 'vunor/vite'
plugins: [vue(), UnoCSS(), Components({ resolvers: [VunorVueResolver] })]
// OR, for Nuxt 3
modules: ['vunor/nuxt']
<!-- App.vue -->
<html class="scope-primary">
<body class="layer-0">
<VuCard level="h3">
<VuCardHeader>Hello</VuCardHeader>
<VuButton class="c8-filled">Save</VuButton>
</VuCard>
</body>
</html>
See references/setup.md for the full setup.
How to use this skill
Load only the reference file you need. Each file is self-contained.
| File | Load when... |
|---|---|
| references/setup.md | Installing Vunor, configuring Vite or Nuxt, listing package exports |
| references/colors.md | Using scope-*, layer-*, surface-*, current-*, bg-current; understanding light/dark behavior |
| references/theme.md | Tuning palette (vividness, saturation, flatness, layersDepth, lightest/darkest), defining custom surfaces, configuring fingertip, baseRadius, typography, animations |
| references/typography.md | Choosing typography utilities, using golden-ratio spacing tokens, applying text-mt-* / text-mb-* margin correction, sizing touch targets with fingertip-* |
| references/cards.md | Building cards: <VuCard>, card shortcut, card-{level} rule, --card-spacing, density, rounded corners, header levels |
| references/shortcuts.md | Customizing or overriding component styles, understanding c8 (clickable) and i8 (input) systems, using defineShortcuts / mergeVunorShortcuts / vunorShortcuts |
| references/rules.md | Looking up a specific UnoCSS rule pattern provided by Vunor (scope-*, current-*, card-*, fingertip-*, i8-*, icon-*, text-m*-*) |
| references/components.md | Using non-form components: VuCard, VuButton, VuDialog, VuTabs, VuMenu, VuPopover, VuAppLayout, VuAppToasts, VuIcon, VuLoadingIndicator, VuPagination, VuProgressBar |
| references/forms.md | Using form components: VuInput, VuSelect, VuCombobox, VuCheckbox, VuRadioGroup, VuSlider, VuDatePicker |
Quick reference
// Preset & shortcuts
import { presetVunor, vunorShortcuts, defineShortcuts,
mergeVunorShortcuts, toUnoShortcut } from 'vunor/theme'
import type { TVunorPaletteOptions, TVunorPaletteColor,
TVunorMainPaletteAdvanced, TVunorLayerPaletteAdvanced,
TVunorSurfaceConfig, TVunorTheme,
TVunorShortcut } from 'vunor/theme'
// PI composables (provide/inject)
import { useInputPi, useInputProps, useInputBaseProps,
useCardPI } from 'vunor'
import { useProvideInject } from 'vunor/utils'
// Vue / Nuxt integration
import { VunorVueResolver } from 'vunor/vite' // unplugin-vue-components resolver
// nuxt.config: modules: ['vunor/nuxt']
// Components (auto-imported when resolver/module is set up)
// <VuButton>, <VuCard>, <VuCardHeader>, <VuCardInner>, <VuDialog>, <VuTabs>,
// <VuMenu>, <VuMenuItem>, <VuPopover>, <VuAppLayout>, <VuAppToasts>,
// <VuIcon>, <VuLoadingIndicator>, <VuInnerLoading>, <VuLabel>, <VuPagination>,
// <VuProgressBar>, <VuOverflowContainer>, <VuCalendar>,
// <VuInput>, <VuInputBase>, <VuSelect>, <VuCombobox>, <VuCheckbox>,
// <VuRadioGroup>, <VuSlider>, <VuDatePicker>, <VuDevTools>
Cheatsheet of semantic classes
<!-- palette scope -->
<div class="scope-primary | scope-error | scope-good | scope-warn |
scope-secondary | scope-grey | scope-neutral">…</div>
<!-- depth backgrounds (auto light/dark) -->
<div class="layer-0">…</div> <!-- 0 outermost, 4 innermost -->
<!-- colored blocks (semantic background+text+border bundle) -->
<div class="surface-0">…</div> <!-- = layer-0 -->
<div class="surface-50 | surface-100 | … | surface-900">…</div>
<!-- direct CSS-var painting -->
<div class="current-bg-scope-color-500 current-text-white">
<span class="bg-current text-current">…</span>
</div>
<div class="bg-scope-color-500/50 text-scope-light-1">…</div>
<!-- tone slots (set automatically by layer-X / surface-X) -->
<span class="text-current">primary</span>
<span class="text-current-muted">secondary / placeholder weight</span>
<span class="text-current-hl">accent (scope-color-500)</span>
<button class="border-current hover:border-current-hover hover:bg-current-hover">hover-step</button>
<!-- clickable styles -->
<button class="c8-filled | c8-flat | c8-outlined | c8-light | c8-chrome">…</button>
<!-- c8-chrome stays neutral inside any scope (use for Cancel / Select all / None
buttons sitting next to a scoped primary CTA) -->
<!-- c8-flat-selected fires on data-selected | aria-selected | aria-pressed | data-on -->
<!-- input styles -->
<div class="i8 i8-filled | i8-flat | i8-round">…</div>
<!-- public composition primitives (compose on hand-rolled elements) -->
<button class="scope-primary c8-filled btn">Save</button> <!-- layout glue for clickables -->
<button class="c8-flat btn btn-square"><span class="i--x btn-icon"/></button>
<input class="i8-bare h-fingertip-m px-$m" /> <!-- standalone input, no .i8 wrapper -->
<div class="popup-card w-[14em]"> <!-- popover surface chrome -->
<button class="menu-item">Row</button>
</div>
<button class="disabled-soft" aria-disabled="true">…</button> <!-- canonical visual disabled -->
<!-- card -->
<VuCard level="h3" rounded dense>…</VuCard>
<!-- spacing & typography -->
<p class="text-h1 text-mb-$m">Title</p> <!-- font-aware margin -->
<div class="p-$m gap-$s h-fingertip rounded-base">…</div>
Ground rules
- Manage color through
scope-*, not hard-coded palette names. The page chrome stays neutral by default (scope-neutralis preflight-installed on:root). Applyscope-primary(orscope-secondary) only on accent elements — primary buttons, focused inputs, brand banners. To reflect state, switch toscope-errorfor negatives/destructive actions,scope-warnfor warnings,scope-goodfor success. - Inside components, prefer scope-relative classes over fixed colors. Reach for
bg-current,text-current,border-current,text-current-hl,bg-current/10, orbg-scope-color-500— notbg-primary-500. That way the same component re-tints automatically when its parent scope changes (<button class="c8-filled">works in any scope;<button class="bg-primary-500">does not). Use specific palette colors (bg-primary-500,text-error-700) only when you genuinely need a scope-independent color. - Don't write
<style>blocks or scoped styles. Compose UnoCSS utilities and Vunor shortcuts. To customize component appearance, override its shortcut viavunorShortcuts(myOverrides)— see references/shortcuts.md. - Layers and surfaces handle dark mode for you — they read
--scope-light-*in light mode and--scope-dark-*under.dark/prefers-color-scheme: dark. Don't adddark:prefixes to layer/surface utilities. - Spacing tokens use a
$prefix:p-$m,gap-$s,m-$l. Plainp-mis a different (built-in UnoCSS) utility. - Touch targets default to
--v-fingertip(3em). Buttons/inputs useh-fingertip; override per-subtree withfingertip-xs|s|m|l|xl. scope-{name}only sets variables. It paints nothing on its own — combine withbg-current,layer-*,surface-*,c8-*,i8-*, etc.