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.
Basic Usage
Section titled “Basic Usage”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.
Property Mapping
Section titled “Property Mapping”Tailwind class names map to camelCase sz prop keys:
| Tailwind class | sz 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 } |
Boolean Props
Section titled “Boolean Props”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}} />Nested Variants
Section titled “Nested Variants”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 Breakpoints
Section titled “Responsive Breakpoints”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+ },}} />Dark Mode
Section titled “Dark Mode”<div sz={{ bg: 'white', color: 'gray-900', dark: { bg: 'gray-800', color: 'white', },}} />Arbitrary Values
Section titled “Arbitrary Values”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]}} />CSS Variables
Section titled “CSS Variables”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)}} />Arbitrary CSS (css: {})
Section titled “Arbitrary CSS (css: {})”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.
Conditional Values
Section titled “Conditional Values”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 runtimeArray Syntax
Section titled “Array Syntax”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 }]} />Shared Style Objects
Section titled “Shared Style Objects”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' winsMultiple 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"Conditional Spread with Static Overrides
Section titled “Conditional Spread with Static Overrides”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).
Combining with className
Section titled “Combining with className”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} />Runtime Helpers for Dynamic Classes
Section titled “Runtime Helpers for Dynamic Classes”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')} />