Skip to content

Sz Props Basics

The sz prop lets you write Tailwind CSS as a JavaScript object. The build plugin transforms it to a className string at compile time — zero runtime cost for static values.

Every Tailwind utility has a corresponding sz prop key:

// Tailwind string syntax
<div className="p-4 bg-blue-500 text-white rounded-lg" />
// Equivalent sz prop syntax — TypeScript validates each key and value
<div sz={{ p: 4, bg: 'blue-500', color: 'white', rounded: 'lg' }} />

Both produce identical HTML. In production, the classes are mangled to single characters.

Tailwind class names map to camelCase sz prop keys:

Tailwind classsz prop
p-4{ p: 4 }
px-6{ px: 6 }
bg-blue-500{ bg: 'blue-500' }
text-white{ color: 'white' }
rounded-lg{ rounded: 'lg' }
font-bold{ fontWeight: 'bold' }
flex{ flex: true }
hidden{ hidden: true }

Many Tailwind utilities are boolean — they’re either on or off:

<div sz={{
flex: true, // flex
hidden: true, // hidden
uppercase: true, // uppercase
italic: true, // italic
truncate: true, // truncate
}} />

Variants (hover, focus, responsive breakpoints) use nested objects:

<button sz={{
bg: 'blue-500',
color: 'white',
px: 4,
py: 2,
rounded: 'md',
hover: {
bg: 'blue-600',
},
focus: {
outline: 'none',
ring: 2,
ringColor: 'blue-400',
},
disabled: {
opacity: 50,
cursor: 'not-allowed',
},
}} />

Responsive modifiers are nested objects with the breakpoint as the key:

<div sz={{
w: 'full', // width: 100% on mobile
md: {
w: '1/2', // width: 50% at md+
},
lg: {
w: '1/3', // width: 33% at lg+
},
}} />
<div sz={{
bg: 'white',
color: 'gray-900',
dark: {
bg: 'gray-800',
color: 'white',
},
}} />

For one-off values not in the Tailwind scale, pass a string. The compiler wraps it in [...] automatically:

<div sz={{
w: '333px', // w-[333px]
bg: '#316ff6', // bg-[#316ff6]
p: '1.25rem', // p-[1.25rem]
top: '37px', // top-[37px]
}} />

Prefix any CSS custom property with -- and the compiler wraps it in (...):

<div sz={{
bg: '--my-brand-color', // bg-(--my-brand-color)
color: '--text-primary', // text-(--text-primary)
p: '--spacing-lg', // p-(--spacing-lg)
}} />

For CSS properties with no sz prop or Tailwind utility equivalent, use the css escape-hatch. Keys are camelCase CSS properties; the compiler converts them to [prop:value] arbitrary-property classes automatically.

<div sz={{
css: {
writingMode: 'vertical-lr', // [writing-mode:vertical-lr]
touchAction: 'none', // [touch-action:none]
'--my-color': 'red', // [--my-color:red]
},
hover: {
css: { cursor: 'crosshair' }, // hover:[cursor:crosshair]
},
md: {
css: { writingMode: 'horizontal-tb' }, // md:[writing-mode:horizontal-tb]
},
}} />

The css key accepts all CSS.Properties keys plus CSS custom properties (--*) — full IDE autocomplete and typo protection.

Pass a ternary expression as any property value. When both branches are static literals (string, number, boolean), the compiler compiles each branch at build time and emits a conditional class expression — no CSS variables, no inline styles:

<div sz={{
bg: isActive ? 'blue-500' : 'gray-200',
color: hasError ? 'red-600' : 'gray-900',
scale: shrunk ? 75 : 100,
}} />
// Compiler emits:
// className={`bg-blue-500 text-red-600 ${shrunk ? 'scale-75' : 'scale-100'}` …}
// (each ternary prop compiled independently, static props merged into a single string)

Works inside variant blocks too:

<div sz={{
p: 4,
hover: { scale: isHovered ? 110 : 100 },
}} />
// hover branch: hover:scale-110 or hover:scale-100 — zero runtime

Pass an array to sz to compose multiple style objects. Fully static arrays are merged at compile time — no runtime cost. Arrays with conditional elements use _szMerge at runtime.

// Fully static — compiled to a single className string
<div sz={[{ p: 4 }, { bg: 'blue-500' }, { rounded: 'lg' }]} />
// Conditional — falsy elements are skipped at runtime
<div sz={[
{ p: 4, color: 'white' },
isActive && { bg: 'blue-500' },
hasError && { ring: 2, ringColor: 'red-500' },
]} />

Array syntax is especially useful with szv() variant objects:

import { szv } from 'csszyx';
const btn = szv({
base: { px: 4, py: 2, rounded: 'md' },
variants: { intent: { primary: { bg: 'blue-600', color: 'white' } } },
});
<button sz={[btn({ intent: 'primary' }), isLoading && { opacity: 50 }]} />

Pass a variable directly to sz when no properties need overriding — the compiler resolves it at build time just like an inline object:

const item = { p: 3, rounded: 'md', bg: 'white' } as const;
<div sz={item} /> // → className="p-3 rounded-md bg-white"

Use object spread (sz={{ ...var, ... }}) only when you need to override or add properties. Last key wins:

const card = { p: 6, rounded: 'xl', shadow: 'md' } as const;
<div sz={{ ...card, p: 4 }} /> // p: 4 overrides card's p: 6
<div sz={{ shadow: 'sm', ...card }} /> // card's shadow: 'md' wins

Multiple spreads and nested variant objects are also resolved statically at build time:

const layout = { flex: true, gap: 4 };
const colors = { bg: 'blue-500', color: 'white' };
<div sz={{ ...layout, ...colors, hover: { opacity: 75 } }} />
// → className="flex gap-4 bg-blue-500 text-white hover:opacity-75"

When the base style depends on a runtime condition but additional properties are fixed, spread the ternary inline. The compiler hoists the condition outward and resolves each branch separately — still zero runtime cost:

const active = { bg: 'blue-500', color: 'white' } as const;
const inactive = { bg: 'gray-100', color: 'gray-600' } as const;
// rotate is always 45 — compiler emits: isActive ? "bg-blue-500 text-white rotate-45" : "bg-gray-100 text-gray-600 rotate-45"
<div sz={{ ...(isActive ? active : inactive), rotate: 45 }} />

This works as long as:

  • Exactly one conditional spread (...(cond ? a : b))
  • The static overrides are compile-time values (literals, variables)

When either condition isn’t met, the compiler falls back gracefully (see below).

The sz prop and className prop can coexist. The compiler merges them:

<div
sz={{ p: 4, bg: 'blue-500' }}
className="custom-class"
/>
// → className="p-4 bg-blue-500 custom-class"

When className is a dynamic expression (not a static string), the compiler uses _szMerge at runtime to join them safely:

// className is dynamic → _szMerge at runtime
<div sz={{ p: 4 }} className={baseClass} />

When class names depend on runtime values, use the helper functions:

import { _sz, _szIf, _szSwitch } from 'csszyx';
// Concatenate multiple class strings
<div className={_sz('p-4 bg-blue-500', extraClass)} />
// Conditional: apply class only when condition is true
<div className={_szIf(isActive, 'ring-2 ring-blue-400')} />
// Switch: first truthy condition wins
<div className={_szSwitch([
[variant === 'primary', 'bg-blue-600 text-white'],
[variant === 'danger', 'bg-red-600 text-white'],
], 'bg-gray-500 text-gray-900')} />