The runtime package provides helpers for composing className strings
dynamically. For static sz props, no helpers are needed — the compiler
produces plain strings at build time.
Runtime helpers
szr · szcn · szDecode · splitBox + class toolkit · SSR hydration validator
~0B Runtime Cost
Static sz props — zero overhead. Helpers only ship when you use them.
Tree Shakeable ESM
Import only what you use. Dead code eliminated at build time.
Resolves multiple class strings or SzObjects into a single mangle-aware className.
Falsy values (false, null, undefined) are skipped. szr is the public,
hand-written name; _sz is the identical helper the compiler injects (the _
marks generated code — do not hand-author it). Reach for szr when you build a
className from szv factory output (e.g. a code-split layout that resolves
variants at the leaf). szr concatenates; to merge with last-wins override on a
same-utility conflict use szcn (mangle-aware className merge).
import{
constszr: (...classes: SzInput[])=> string
Back-compat szr — the public, hand-written name for
_sz
. See
coreSz
for the full contract (szr vs szcn, falsy handling).
Creates a variant-based sz object factory with strong TypeScript inference.
TypeScript catches invalid variant values at compile time — no runtime
surprises. All variant objects are plain sz objects, fully compatible
with the sz prop and
@csszyx ― /dynamic's sz() function.
@param ― config - Variant configuration with base, variants, and defaultVariants
@returns ― A factory function that accepts a variant selection and returns an SzObject
Creates a variant-based sz object factory with strong TypeScript inference.
TypeScript catches invalid variant values at compile time — no runtime
surprises. All variant objects are plain sz objects, fully compatible
with the sz prop and
@csszyx ― /dynamic's sz() function.
@param ― config - Variant configuration with base, variants, and defaultVariants
@returns ― A factory function that accepts a variant selection and returns an SzObject
The main @csszyx/runtime entry stays fully standalone: szr({ p: 4 }) works
with no plugin and no extra import, and that guarantee is exactly why importing
szr from it ships the browser transform (~13 KB gz) — the object branch
must be able to lower anything. The build removes that cost automatically
whenever it can PROVE objects never flow:
@csszyx/runtime/core — the concat family (szr, _sz, _sz2, _sz3,
__szvPick, __szvPick1), string-first, no compiler (~0.6 KB gz). When every szr(...)
argument in a file is provably a string or falsy — string/template literals,
false/null/undefined, &&/ternaries whose reachable results are all
safe — the compiler retargets the import here on its own, splitting a mixed
clause like import { szr, szv } so the rest stays put. Anything uncertain
(an identifier, a call, x as string) keeps today’s import.
@csszyx/runtime/merge — the group-merge family (_szcn, _szPart,
_szMerge, ~5 KB of merge tables, no compiler). Injected instead of the
main entry when every dynamic sz array element is provably a string.
@csszyx/runtime/lowering — one bare side-effect import that makes the
slim helpers object-capable. The plugin handles it; outside the plugin
pipeline (unit tests, scripts) add import '@csszyx/runtime/lowering' once
at startup. An object reaching a slim helper without it throws an error that
names this exact line — never silently unstyled markup.
szv factories join in: a file-local (or, in production builds, an IMPORTED)
factory with a fully literal config compiles per key — a static selection
becomes the final class string at build time, a dynamic one becomes a
__szvPick(table, selection) lookup (~40× faster per render than the object
path) — as long as no two co-occurring branches touch the same property, since
object merge keeps one class where concatenation keeps both. A call that
selects exactly ONE dimension by a literal name, F({ direction: dir }),
narrows further to __szvPick1(table, "direction", dir), which skips both the
per-render selection object and the walk over every other dimension (measured
~4× faster again on a five-dimension table). That narrowing needs the table to
carry no defaultVariants, since a default makes the omitted dimensions
contribute classes too. The cross-module half runs in production builds only
and resolves RELATIVE import specifiers; dev keeps the unoptimized behavior so
nothing can go stale under HMR.
All of it is conservative by construction: every uncertain shape keeps the
current code, so the worst case is byte-for-byte today’s output. Mirrors of
core and merge exist as csszyx/core and the umbrella re-exports.
Merges className strings with last-wins override per utility — the merge to
use at the single resolution point of a layered component (typically the leaf
Box), and for merging a part’s own defaults with a consumer override. Falsy
inputs (false, null, undefined, '') are skipped.
npm tailwind-merge cannot do this job here: in a production build csszyx
mangles owned classes (gap-2 → q3, gap-8 → q7), and tailwind-merge
can’t tell q3/q7 are the same utility. szcn decodes each token through the
runtime reverse mangle map (window.__csszyx.decode) before grouping, so
overrides keep working with mangling on.
functionszcn(...inputs: ClassInput[]): string
Merge className strings with last-wins override per utility, mangle-aware and
memoized (see the memo note above — repeated inputs return in one Map lookup).
Intended for the single resolution point in a layered design-system component
(typically at the leaf Box): combine the component's default classes with the
forwarded override so the override wins on a same-utility collision, while
keeping production mangling intact (unlike npm tailwind-merge).
@param ― inputs - Class strings; falsy inputs (false/null/undefined/'') are skipped.
Merge className strings with last-wins override per utility, mangle-aware and
memoized (see the memo note above — repeated inputs return in one Map lookup).
Intended for the single resolution point in a layered design-system component
(typically at the leaf Box): combine the component's default classes with the
forwarded override so the override wins on a same-utility collision, while
keeping production mangling intact (unlike npm tailwind-merge).
@param ― inputs - Class strings; falsy inputs (false/null/undefined/'') are skipped.
szcn('md:gap-2','gap-8');// → 'md:gap-2 gap-8' variants isolate — both kept
Fail-safe contract: a token whose conflict group can’t be determined
confidently is NEVER merged away — it keys by itself, so the worst case is two
classes co-existing (the pre-merge status quo), never a wrongly-dropped class.
A shorthand appearing later removes the longhands it subsumes; a longhand
appearing later only refines. Covers padding, margin, inset, and border-radius:
szcn('pb-4','p-8');// → 'p-8' p covers pb
szcn('p-4','pb-8');// → 'p-4 pb-8' pb only refines the bottom
Logical sides (ps/pe, ms/me) are subsumed by their p/m shorthands.
For inset and rounded the coverage is physical-only: inset/rounded do
not subsume logical start/end / rounded-s* tokens, which could flip under
RTL — those stay keep-both (still cascade-correct).
Eight prefixes span more than one CSS property (text-sm is font-size,
text-red-500 is color). For these, szcn classifies the token value into
a property group: same group → last wins, different group → co-exist,
unclassifiable → keep-both.
// border / divide / ring / outline: width / color / style
szcn('border-2','border-4');// → 'border-4'
// flex: shorthand / direction / wrap
szcn('flex-row','flex-col');// → 'flex-col'
szcn('flex','flex-1');// → 'flex flex-1' bare flex is display
Still keep-both by design: directional/axis forms of border-family prefixes
(border-t-2 vs border-2, divide-x-2 vs divide-y-2), CSS-variable values
(text-(--brand) — the type is unknown), and any value csszyx cannot classify.
On a production-mangled build, szcn also encodes its output. A class name
your component resolves at runtime as a plain string — a prop mapped to
'flex-col', a template like `gap-${n}` — never went through the
compiler, so without encoding it would reach the DOM in its original spelling
while the shipped CSS only contains the mangled selector. szcn closes that
gap: every surviving token is looked up in the runtime mangle map and leaves
the merge in mangled form.
The lookup is single-pass and idempotent. Already-mangled tokens, authored
literals reserved via production.mangleExclude, and external (non-csszyx)
class names pass through unchanged — token allocation guarantees a mangled
token can never spell a censused class name, so one map lookup is unambiguous.
In dev, or on a build without mangling, the encode step is an identity.
Code that inspects a className for a utility by its original spelling must
decode first — see szDecode.
Registers custom token names so szcn can classify classes built from them.
Idempotent and additive; the build plugin calls it automatically from the
app’s @theme blocks — call it manually only for hand-written CSS utilities.
Guard rails — both fall back to keep-both with a one-time dev warning, never a
wrong merge:
A name that shadows a built-in value keyword of an affected prefix is
rejected: a color named cover would make szcn misread bg-cover
(background-size) as a color and merge it wrongly.
A name registered in two conflicting categories (both a color and a text
size — text-huge becomes unclassifiable) is dropped from both, and the drop
is remembered: later registrations of either side are rejected too, so
registration order can never resurrect an ambiguous name into one category.
Maps a mangled class token back to its original name. On any build shape where
the token is not mangled — dev, mangle: false, an authored literal, an
external class — it returns the input unchanged, so it is always safe to call.
functionszDecode(token: string): string
Reach for it whenever code inspects a className for a utility by its
original spelling. String checks like startsWith('w-') silently stop
matching on a mangled build (the DOM carries q3, not w-full); decoding
first keeps the check correct on every build:
Variant prefixes decode with the token (szDecode('x7') can return
'md:hover:w-full'), so strip the prefix before comparing the bare utility if
your check targets the base class.
Not a runtime function: the build rewrites every
szs={{…}}
call site to szsc={{ slot: "class string" }}, so the component receives plain
strings on a dedicated prop and forwards them into a child className with no
helper. Declare both faces from one slot union with SzsProps:
CSR recovery is now opted in per-element via the szRecover JSX
attribute ("csr" or "dev-only"); no global flag needed. See
SSR & Hydration → Recovery Tokens.
All variant class combinations are catalogued at build time — the compiler extracts every
possible output and adds them to the Tailwind safelist so CSS is pre-generated.
When a caller passes one flat className to a component that renders nested
elements, the styles often belong on different elements — the margin on the
outer frame, the padding on the inner content. splitBox partitions a className
at the CSS box-model border line into { outer, inner }. Every token lands in
exactly one bucket (no loss, no duplication) and keeps its variant prefix.
functionsplitBox(className: string, options?: {
outer?: BoxSelector[];// force these onto the outer node
inner?: BoxSelector[];// force these onto the inner node
The split follows the box model: outer = border-outward (margin, position,
border, sizing, background, shadow, transform, visibility); inner =
border-inward (padding, overflow, display, layout, gap, text, paint-inside,
interactivity). The class-token → box-role map is generated from the
compiler’s property tables, so it never drifts. Every default is overridable.
The category-aware toolkit exposes csszyx’s class knowledge as primitives.
csszyx owns the truth (a class’s box-role + category); your project owns the
rule (which dependent classes to add, under which conditions) — no rule-DSL,
no hardcoded Tailwind vocabulary.
The sz-object twin of splitBox: partitions an sz object (not a className)
into { outer, inner } sz objects, so a component built with szv stays
sz-native and keeps the compiler’s auto-safelisting. Each key routes to the same
side its emitted class would — splitBoxSz(x) ≡ splitBox(compile(x)).
Arrays are deep-merged (last-write-wins); null / false / undefined and
empty objects yield { outer: {}, inner: {} }. A raw className string has no
sz-object form, so it throws in development.
Removes sz before a component spreads ...rest onto a host element. Compiled
components never carry a leftover sz, but a file that was not compiled
(e.g. a workspace package missing from compileSources) keeps its raw sz,
which leaks to the DOM as sz="[object Object]". stripSzProps drops it and,
in development, warns once when the leaked sz is a raw object.
When production mangling is on, csszyx embeds a mangle map plus a checksum so the
runtime can detect a map that was altered or drifted out of sync.
verifyMangleChecksumAsyncrecomputes the map’s checksum with the Web Crypto
API (crypto.subtle) and compares it to the expected value — no WASM core
required, which makes it usable in edge/serverless runtimes.
functionverifyMangleChecksumAsync(
expectedChecksum: string,
map?: MangleMap, // loaded + schema-validated from the DOM when omitted
):Promise<boolean>// true only when the map is present and its checksum matches