Reusing Styles
CSSzyx resolves style variables at build time — the same way it resolves inline
objects. No runtime overhead, no className="[object Object]".
Direct Reference
Section titled “Direct Reference”The simplest way to reuse a style object — pass the variable directly to sz:
const card = { p: 6, rounded: 'xl', shadow: 'md', bg: 'white' } as const;
// All three components share the same compiled className<div sz={card} /><article sz={card} /><section sz={card} />// → className="p-6 rounded-xl shadow-md bg-white"as const is optional — the compiler resolves annotated and plain objects alike,
declared with either const or let.
Override with Spread
Section titled “Override with Spread”Use sz={{ ...var, key: val }} when some components need different values. Last key
wins, so you can layer overrides cleanly:
const item = { p: 3, rounded: 'md', bg: 'white', border: true, borderColor: 'gray-200' } as const;
// Each variant adds or overrides specific keys<div sz={item} /> // base — p-3<div sz={{ ...item, p: 6 }} /> // larger padding<div sz={{ ...item, bg: 'blue-50', borderColor: 'blue-200' }} /> // tintedArray Composition
Section titled “Array Composition”Pass an array to compose multiple objects. The compiler merges fully-static arrays at
build time; conditional elements use _szMerge at runtime:
const layout = { display: 'flex', items: 'center', gap: 3 } as const;const text = { text: 'sm', weight: 'medium' } as const;
// Static — single className string, zero runtime<div sz={[layout, text]} />
// Conditional — only isActive element is dynamic<div sz={[layout, text, isActive && { bg: 'blue-50' }]} />Variables work as array elements too — no inline objects required:
const active = { bg: 'blue-500', color: 'white' } as const;const disabled = { opacity: 50, cursor: 'not-allowed' } as const;
<button sz={[layout, isActive && active, isDisabled && disabled]} />Ternary Branches
Section titled “Ternary Branches”When a component switches entirely between two styles, use a ternary:
const active = { bg: 'blue-500', color: 'white', shadow: 'md' } as const;const inactive = { bg: 'gray-100', color: 'gray-600' } as const;
<button sz={isActive ? active : inactive} />// Compiler emits: className={isActive ? "bg-blue-500 text-white shadow-md" : "bg-gray-100 text-gray-600"}Both branches are compiled to static strings — no runtime object merging.
More than two options chain the same way, and compile the same way:
<div sz={isDanger ? danger : isWarning ? warning : normal} />// → className={isDanger ? "…" : isWarning ? "…" : "…"}The emitted expression keeps the nesting you wrote, so isWarning is only
evaluated when isDanger is falsy. Up to eight tests compile; past that the
attribute keeps its runtime path, and a set that long is what
szv() is for.
Chained Variables
Section titled “Chained Variables”A variable can reference another variable in its initializer. The compiler resolves the chain recursively:
const base = { p: 4, rounded: 'lg' };const elevated = { ...base, shadow: 'md', bg: 'white' };const featured = { ...elevated, ring: 2, ringColor: 'blue-500' };
<div sz={featured} />// → className="p-4 rounded-lg shadow-md bg-white ring-2 ring-blue-500"Component Patterns
Section titled “Component Patterns”export const cardBase = { p: 6, rounded: 'xl', bg: 'white', border: true, borderColor: 'gray-200', shadow: 'sm',} as const;
// components/Card.tsximport { cardBase } from '../styles/card';
export function Card({ elevated }: { elevated?: boolean }) { return ( <div sz={elevated ? { ...cardBase, shadow: 'lg' } : cardBase}> {/* ... */} </div> );}const label = { text: 'sm', weight: 'medium', color: 'gray-600' } as const;
function SpaceXDemo() { return ( <div sz={{ display: 'flex', gap: 4 }}> <div sz={{ display: 'flex', spaceX: 2 }}> <div sz={item}>A</div> <div sz={item}>B</div> <span sz={{ ...label, ml: 2 }}>spaceX-2</span> </div> </div> );}const btnBase = { px: 4, py: 2, rounded: 'md', weight: 'medium' } as const;const btnPrimary = { bg: 'blue-600', color: 'white', hover: { bg: 'blue-700' } } as const;const btnGhost = { bg: 'transparent', color: 'blue-600', hover: { bg: 'blue-50' } } as const;
function Button({ variant, disabled }: ButtonProps) { return ( <button sz={[ btnBase, variant === 'primary' ? btnPrimary : btnGhost, disabled && { opacity: 50, cursor: 'not-allowed' }, ]}> {children} </button> );}Graceful Fallback
Section titled “Graceful Fallback”The compiler only resolves variables declared in the same file with a static object
literal initializer. Anything else falls back to the _sz() runtime helper — no crash,
no [object Object]:
| Pattern | Resolution |
|---|---|
const x = { ... } (same file) | Build time ✅ |
const x = { ... } as const | Build time ✅ |
const x = { ...other, key: val } | Build time ✅ (recursive) |
sz={{ ...(cond ? a : b), key: val }} | Build time ✅ (conditional spread hoist) |
const x = cond ? a : b | Build time ✅ (both branches, wherever it is written) |
const x = cond && a | Build time ✅ (the else arm is the empty style) |
import { x } from './styles' | Build time ✅ (see Across files) |
const x = getStyles() | Runtime fallback |
const x = cond || a | Runtime fallback (|| keeps its left operand, which may itself be a style) |
In development, the compiler emits a build-time warning for any pattern it
falls back to _sz() — explaining why the fallback occurred and pointing to
the right alternative (szv() for static variants, dynamic() for runtime
data). The warning appears in the Vite/webpack dev-server output, not at runtime.
Across files
Section titled “Across files”An import is resolved when the compiler can follow the name to a literal object in the module that declares it. What decides is that last hop, not the import syntax — so the shape of the export is what to look at when a style does not fold.
| Provider module | Importer | Resolution |
|---|---|---|
export const x = { ... } | import { x } from './styles' | Build time ✅ |
const x = { ... } + export { x } | import { x } from './styles' | Build time ✅ |
export const x = { ... } | import * as S from './styles', sz={S.x} | Build time ✅ |
export default { ... } | import x from './styles' | Build time ✅ |
export { x } from './base' (barrel) | import { x } from './styles' | Build time ✅ |
export { x as y } from './base' | import { y } from './styles' | Build time ✅ |
import { x } from './base' + export { x } | import { x } from './styles' | Build time ✅ |
const x = { ... } + export default x | import x from './styles' | Runtime fallback |
export * from './base' | import { x } from './styles' | Runtime fallback |
export const x = getStyles() | import { x } from './styles' | Runtime fallback |
A re-export is read as a link rather than a value and followed to the module that declares it, so importing through a barrel compiles exactly like importing the provider directly — including a chain of barrels, up to eight modules deep.
The three that fall back do so for three different reasons. export * names no
export, so there is nothing to file a value under without reading the provider’s
whole export list. export default x puts an identifier in the default slot
rather than a literal. And a call is not a value the compiler can read at all. A
runtime fallback here is missing output — the classes were never collected
anywhere, so the CSS does not exist. The build reports it by name.
Reading imported objects is build.importedStaticSz,
which is on by default; turning it off makes every row above a runtime fallback.
Component Variant Libraries
Section titled “Component Variant Libraries”For predefined variant systems (intent, size, state…), use
szv() instead of manual conditionals:
import { szv } from 'csszyx';
const btn = szv({ base: { px: 4, py: 2, rounded: 'md', weight: 'medium' }, variants: { intent: { primary: { bg: 'blue-600', color: 'white' }, ghost: { bg: 'transparent', color: 'blue-600' }, }, size: { sm: { text: 'sm', px: 3, py: 1.5 }, lg: { text: 'lg', px: 6, py: 3 }, }, }, defaultVariants: { intent: 'primary', size: 'sm' },});
<button sz={btn({ intent: 'ghost', size: 'lg' })} />szv() handles the conditional logic and TypeScript inference — you define styles once,
use them everywhere.