szv() — Variant Authoring
szv() is the csszyx equivalent of CVA (class-variance-authority). It returns a factory
function that produces sz objects — keeping DX consistent with the sz prop throughout.
Basic usage
Section titled “Basic usage”import { szv } from 'csszyx';
const buttonSz = szv({ base: { display: 'inline-flex', items: 'center', rounded: 'md', weight: 'medium' }, variants: { variant: { default: { bg: 'primary', text: 'primary-foreground' }, destructive: { bg: 'destructive', text: 'destructive-foreground' }, outline: { border: true, borderColor: 'blue-500', bg: 'transparent' }, ghost: { hover: { bg: 'accent' } }, }, size: { sm: { h: 9, px: 3, text: 'sm' }, md: { h: 10, px: 4 }, lg: { h: 11, px: 8 }, icon: { h: 10, w: 10 }, }, }, defaultVariants: { variant: 'default', size: 'md' },});
// Usage — returns an sz object, feeds directly into sz prop<button sz={buttonSz({ variant: 'outline', size: 'sm' })} />
// defaultVariants apply when no selection given<button sz={buttonSz()} />TypeScript inference — no annotations needed
Section titled “TypeScript inference — no annotations needed”szv() infers all variant keys and valid values from the config object.
TypeScript catches invalid values at the call site without any manual type annotations:
buttonSz({ variant: 'outline' }) // ✅ validbuttonSz({ variant: 'invalid' }) // ❌ TypeScript error: '"invalid"' not assignablebuttonSz({ size: 'xl' }) // ❌ TypeScript error: '"xl"' not assignablebuttonSz({}) // ✅ defaultVariants applybuttonSz() // ✅ same as {}Compose with sz array syntax
Section titled “Compose with sz array syntax”szv() output is a plain sz object — compose it with conditional styles using the array syntax:
<button sz={[ buttonSz({ variant: props.variant, size: props.size }), isLoading && { opacity: 50, cursor: 'wait' }, isDisabled && { opacity: 50, cursor: 'not-allowed' },]} />The compiler handles static items at build time; conditionals use _szMerge at runtime.
Nested variants are deep merged
Section titled “Nested variants are deep merged”Base hover styles are preserved when a variant adds its own hover:
const cardSz = szv({ base: { rounded: 'lg', hover: { shadow: 'md' } }, variants: { color: { blue: { bg: 'blue-50', hover: { bg: 'blue-100' } }, red: { bg: 'red-50', hover: { bg: 'red-100' } }, }, },});
cardSz({ color: 'blue' })// → { rounded: 'lg', bg: 'blue-50', hover: { shadow: 'md', bg: 'blue-100' } }// ^^^^^^^^^^^ ↑ base keptnull / undefined falls back to defaultVariants
Section titled “null / undefined falls back to defaultVariants”Passing null or undefined for a variant key is treated as “not specified” —
the defaultVariant applies instead of clearing the style:
<button sz={buttonSz({ variant: props.variant ?? null })} />// If props.variant is null/undefined → defaultVariants.variant ('default') appliesWith @csszyx/dynamic
Section titled “With @csszyx/dynamic”szv() works identically in runtime-injection mode:
import { useSz } from 'csszyx/dynamic/react';
const { sz } = useSz();<button className={sz(buttonSz({ variant: 'outline' }))} />No base required
Section titled “No base required”const pill = szv({ variants: { size: { sm: { px: 2, py: 1, text: 'xs' }, lg: { px: 4, py: 2, text: 'base' }, }, }, defaultVariants: { size: 'sm' },});
<span sz={pill()} /> // size: 'sm' (default)<span sz={pill({ size: 'lg' })} />Build-time extraction & co-location
Section titled “Build-time extraction & co-location”The compiler reads every variant class from the szv({…}) config literal at
its declaration — regardless of how the returned factory is later used (directly
in sz=, through splitBoxSz, or not at all). So routing the output through a
helper never loses the CSS.
The prescan discovers a file by a cheap text check: it scans any file containing
sz=, sz:, or a szv( declaration. So a standalone file holding only a
szv({…}) config (no JSX, no sz=) is still scanned and its variants extracted.
Extraction is lenient per key, identically in all three parser engines. A
value the compiler cannot read statically (a function call, a template string,
an imported constant…) skips only that one key — sibling keys and every other
variant still reach the safelist. A finite conditional like
p: dense ? 2 : 4 contributes both branches (the runtime picks one, so
both classes must exist). null/undefined mean “key unset”. Same-file
const references and const object spreads are followed
(mx: GUTTER, { ...shared, mx: 0 }); a reassigned let never is.
Co-locating each szv in the component file that uses it is still the
recommended layout — it makes the component the unit of tree-shaking. In a
monorepo, opt a workspace package in with
compileSources so its szv is scanned too.
Config validation (development)
Section titled “Config validation (development)”In development, szv validates the config shape and the selection and warns once
on a problem — a missing variants, a non-object variant value, or a selection
that names an unknown variant or value. It degrades safely (a broken config falls
back to base / {} rather than throwing per render). TypeScript catches these
at author time; the runtime guard covers configs built from JSON / as any / a
plain-JS caller. Production is a no-op.
Dynamic & specificity
Section titled “Dynamic & specificity”szv returns a plain sz object — it does not run through dynamic(). Used as
sz={szv(…)} it is fully build-time. Only wrap it in dynamic() for genuinely
runtime values; see Build-Time vs Runtime.