Appearance
Button
A button with three independent variant axes: color, style, and size. The color × style matrix is expressed via compoundVariants, one explicit rule per (color, style) cell, which keeps the recipe direct and the call sites readable.
Authoring
ts
// recipes/button.config.ts
import { defineComponent } from 'varia'
export default defineComponent('btn', {
base: [
'inline-flex items-center justify-center rounded-md font-medium border',
'transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
'disabled:opacity-50 disabled:cursor-not-allowed',
],
variants: {
c: {
primary: 'focus-visible:ring-blue-500',
success: 'focus-visible:ring-emerald-500',
danger: 'focus-visible:ring-red-500',
warning: 'focus-visible:ring-amber-500',
neutral: 'focus-visible:ring-gray-500',
},
style: {
solid: 'text-white',
outline: 'bg-transparent',
subtle: 'border-transparent',
ghost: 'bg-transparent border-transparent',
},
s: {
sm: 'px-2.5 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
},
compoundVariants: [
// primary (blue)
{ when: { c: 'primary', style: 'solid' }, class: 'bg-blue-600 border-blue-600 hover:bg-blue-700' },
{ when: { c: 'primary', style: 'outline' }, class: 'text-blue-700 border-blue-300 hover:bg-blue-50' },
{ when: { c: 'primary', style: 'subtle' }, class: 'bg-blue-50 text-blue-700 hover:bg-blue-100' },
{ when: { c: 'primary', style: 'ghost' }, class: 'text-blue-700 hover:bg-blue-50' },
// success (emerald)
{ when: { c: 'success', style: 'solid' }, class: 'bg-emerald-600 border-emerald-600 hover:bg-emerald-700' },
{ when: { c: 'success', style: 'outline' }, class: 'text-emerald-700 border-emerald-300 hover:bg-emerald-50' },
{ when: { c: 'success', style: 'subtle' }, class: 'bg-emerald-50 text-emerald-700 hover:bg-emerald-100' },
{ when: { c: 'success', style: 'ghost' }, class: 'text-emerald-700 hover:bg-emerald-50' },
// danger (red), warning (amber), neutral (gray) follow the same shape;
// see the source for the full 20-entry list.
],
})Two things to call out:
candstylecarry only the constant properties.c.primarysetsfocus-visible:ring-blue-500, which is the same regardless of style.style.solidsetstext-white, the same regardless of color. Everything that depends on both axes (background, border, hover background, the colored text in outline/subtle/ghost) lives in the compound rules.Spell every class name out as a literal. Tailwind and UnoCSS both recommend against constructing class names with template literals (
`bg-${tone}-600`). The JIT extractor scans your source text for class candidates and only sees complete string literals —bg-${tone}-600never gets resolved, and the theme variables those classes would have triggered (--colors-emerald-600, etc.) never get emitted. Five colors × four styles is twenty rows; that's the price of staying inside the tools' supported usage.
Live preview
All four styles, primary color, three sizes:
Consumption
html
<button class="btn btn-c-primary btn-style-solid btn-s-md">Save</button>
<button class="btn btn-c-danger btn-style-outline btn-s-md">Delete</button>
<button class="btn btn-c-success btn-style-subtle btn-s-md">Continue</button>
<button class="btn btn-c-neutral btn-style-ghost btn-s-md">Cancel</button>
<button class="btn btn-c-primary btn-style-solid btn-s-md" disabled>Loading...</button>Three classes per button: color, style, size. The base class (btn) carries state styling (hover:, focus-visible:, disabled:) once for all combinations.
Generated class names
| Class | Purpose |
|---|---|
btn | Base styling (state, transitions, focus ring scaffolding) |
btn-c-primary / -success / -danger / -warning / -neutral | Color (carries focus-ring tint, used as a compound-match key) |
btn-style-solid / -outline / -subtle / -ghost | Style (carries the cross-color constant for that style) |
btn-s-sm / -md / -lg | Size |
The compound rules don't get their own consumer-facing class names; they fire automatically when both btn-c-* and btn-style-* are present on the same element.
Customizing
Three common edits:
- To remap a color to a different palette tone, rewrite the four
c: '<name>'compound rows with the new tone (e.g.bg-green-600instead ofbg-blue-600). It's a search-and-replace within the four primary entries. - To add a new color, add a
c.accent: 'focus-visible:ring-purple-500'shortcut and append four new compound rows forc: 'accent'× each style. - To add a new style, add a
style.link: ...shortcut and append a new compound row for each color (one row per color).
When compound variants are the right shape
The button uses compoundVariants because the color × style matrix has a genuine cross-axis dependency: the background color of a "solid primary" button is blue-600, but a "solid danger" button is red-600, and an "outline primary" button has no background at all. There's no way to compute that from c alone or style alone; the cell value needs both axes.
The trade-off: compound rules emit unconditionally (they're preflight CSS, not JIT shortcuts), so all 20 ship in your bundle even if your page only uses two of them. At this matrix size, that's a few hundred bytes; at much larger matrices (every Tailwind palette color, say) it would be worth reconsidering.
The Icon button recipe shows compound variants with a different shape: square × size, where the compound is essential because each cell sets a different CSS property (padding, not just a different padding value).