Skip to content

Runtime Helpers

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.

import { szr, szcn } from '@csszyx/runtime';

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 {
const szr: (...classes: SzInput[]) => string

Back-compat szr — the public, hand-written name for

_sz

. See

coreSz

for the full contract (szr vs szcn, falsy handling).

szr
,
function szv<V extends VariantSchema>(config: SzvConfig<V>): (selection?: VariantSelection<V>) => 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.

@paramconfig - Variant configuration with base, variants, and defaultVariants

@returnsA factory function that accepts a variant selection and returns an SzObject

@example

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' },
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 },
},
},
defaultVariants: { variant: 'default', size: 'md' },
});
// Usage — consistent with sz prop, TypeScript catches invalid values
<button sz={buttonSz({ variant: 'outline', size: 'sm' })} />
// Compose with sz array syntax
<button sz={[
buttonSz({ variant: props.variant, size: props.size }),
isLoading && { opacity: 50, cursor: 'wait' },
]} />
// With

@csszyx

/dynamic for fully runtime-resolved styling const { sz } = useSz(); <button className={sz(buttonSz({ variant: props.variant }))} />

szv
} from '@csszyx/runtime';
const
const cardSz: (selection?: VariantSelection<{
pad: {
lg: {
p: number;
};
};
}> | undefined) => SzObject
cardSz
=
szv<{
pad: {
lg: {
p: number;
};
};
}>(config: SzvConfig<{
pad: {
lg: {
p: number;
};
};
}>): (selection?: VariantSelection<{
pad: {
lg: {
p: number;
};
};
}> | undefined) => 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.

@paramconfig - Variant configuration with base, variants, and defaultVariants

@returnsA factory function that accepts a variant selection and returns an SzObject

@example

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' },
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 },
},
},
defaultVariants: { variant: 'default', size: 'md' },
});
// Usage — consistent with sz prop, TypeScript catches invalid values
<button sz={buttonSz({ variant: 'outline', size: 'sm' })} />
// Compose with sz array syntax
<button sz={[
buttonSz({ variant: props.variant, size: props.size }),
isLoading && { opacity: 50, cursor: 'wait' },
]} />
// With

@csszyx

/dynamic for fully runtime-resolved styling const { sz } = useSz(); <button className={sz(buttonSz({ variant: props.variant }))} />

szv
({
SzvConfig<{ pad: { lg: { p: number; }; }; }>.variants?: {
pad: {
lg: {
p: number;
};
};
} | undefined
variants
: {
pad: {
lg: {
p: number;
};
}
pad
: {
lg: {
p: number;
}
lg
: {
p: number
p
: 8 } } } });
const
const cls: string
cls
=
function szr(...classes: SzInput[]): string

Back-compat szr — the public, hand-written name for

_sz

. See

coreSz

for the full contract (szr vs szcn, falsy handling).

szr
(
const cardSz: (selection?: VariantSelection<{
pad: {
lg: {
p: number;
};
};
}> | undefined) => SzObject
cardSz
({
pad?: "lg" | null | undefined
pad
: 'lg' }), 'bg-blue-500', false, null, 'text-white');

Usage:

// Basic concatenation
<
any
div
className: string
className
={
function szr(...classes: SzInput[]): string

Back-compat szr — the public, hand-written name for

_sz

. See

coreSz

for the full contract (szr vs szcn, falsy handling).

szr
('p-4', 'bg-blue-500', 'text-white')} />
// Falsy values are skipped automatically
const
const isActive: true
isActive
= true;
const
const hasError: false
hasError
= false;
<
any
div
className: string
className
={
function szr(...classes: SzInput[]): string

Back-compat szr — the public, hand-written name for

_sz

. See

coreSz

for the full contract (szr vs szcn, falsy handling).

szr
('base',
const isActive: true
isActive
&& 'active',
const hasError: false
hasError
&& 'error')} />

Bundle-Slim Entries and Build Optimizations

Section titled “Bundle-Slim Entries and Build Optimizations”

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/split — the className half of the class toolkit (classify, has, pick, omit, splitBox, normalizeBase, stripVariant), without the sz-object adapters. Unlike the entries above, the plugin never retargets to this one — you import it by hand when a project reads Tailwind strings and never passes sz objects to the toolkit. Under ESM the main entry already tree-shakes to within 73 B of it; the saving is on the require() path, where nothing shakes: 25.2 KB gz against 31.2 KB. It does not drop @csszyx/compiler from the dependency graph: both halves of the toolkit live in one module, so dist/split.cjs still requires @csszyx/compiler/browser even though nothing on this entry calls into it.
  • @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.

szcn(...classes) — Mangle-Aware Override Merge

Section titled “szcn(...classes) — Mangle-Aware Override Merge”

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.

function szcn(...inputs: (string | false | null | undefined)[]): string

npm tailwind-merge cannot do this job here: in a production build csszyx mangles owned classes (gap-2q3, gap-8q7), and tailwind-merge can’t tell q3/q7 are the same utility. szcn decodes each token through the runtime mangle registry (getMangleRegistry().decode, registered by the build from inside the bundle) before grouping, so overrides keep working with mangling on.

function szcn(...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).

@paraminputs - Class strings; falsy inputs (false/null/undefined/'') are skipped.

@returnsThe merged className string.

@example szcn('gap-2 p-4', 'gap-8') // → 'p-4 gap-8' (gap-8 overrides gap-2)

szcn
('gap-2 p-4', 'gap-8'); // → 'p-4 gap-8' gap-8 overrides gap-2
function szcn(...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).

@paraminputs - Class strings; falsy inputs (false/null/undefined/'') are skipped.

@returnsThe merged className string.

@example szcn('gap-2 p-4', 'gap-8') // → 'p-4 gap-8' (gap-8 overrides gap-2)

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
szcn('ml-2', 'mx-4'); // → 'mx-4'
szcn('top-2', 'inset-4'); // → 'inset-4'
szcn('rounded-tl-sm', 'rounded-lg'); // → 'rounded-lg'

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).

Sixteen 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.

// text: size / color / align / wrap / overflow
szcn('text-base', 'text-sm'); // → 'text-sm'
szcn('text-red-500', 'text-sm'); // → 'text-red-500 text-sm'
szcn('text-sm/6', 'text-lg/7'); // → 'text-lg/7' modifiers stay in-group
// font: family vs weight
szcn('font-semibold', 'font-normal'); // → 'font-normal'
szcn('font-sans', 'font-bold'); // → 'font-sans font-bold'
// bg: color / size / position / repeat / attachment / clip / image
szcn('bg-red-500', 'bg-blue-500'); // → 'bg-blue-500'
szcn('bg-red-500', 'bg-cover'); // → 'bg-red-500 bg-cover'
// 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
// shadow / drop-shadow / inset-shadow: size vs shadow color
szcn('shadow-sm', 'shadow-lg'); // → 'shadow-lg'
szcn('shadow-lg', 'shadow-red-500'); // → 'shadow-lg shadow-red-500'
// decoration: thickness / style / color
szcn('decoration-2', 'decoration-red-500'); // → 'decoration-2 decoration-red-500'
// stroke: width vs color
szcn('stroke-2', 'stroke-red-500'); // → 'stroke-2 stroke-red-500'
// from / via / to: gradient stop position vs stop color
szcn('from-10%', 'from-red-500'); // → 'from-10% from-red-500'

Setting a shadow’s size and its color together is the documented Tailwind way to write one, so those two never share a group — the same reasoning that keeps border-2 and border-red-500 apart.

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.

Production encode of runtime-resolved classes

Section titled “Production encode of runtime-resolved classes”

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, classes kept by production.manglePreserve, 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.

Custom token names have to be declared before szcn can classify classes built from them. The build plugin does this automatically from your @theme blocks — these functions are for hand-written CSS utilities, and for reading back what is in effect.

interface SzcnThemeGroups {
colors?: readonly string[]; // 'brand' → text-brand, bg-brand, border-brand, …
textSizes?: readonly string[]; // 'huge' → text-huge
fontFamilies?: readonly string[]; // 'display' → font-display
fontWeights?: readonly string[]; // 'chunky' → font-chunky
}
function registerSzcnGroups(groups: SzcnThemeGroups, source?: string): void
function setSzcnGroups(groups: SzcnThemeGroups, source?: string): void
function clearSzcnGroups(source?: string): void
function getSzcnGroups(): Record<'colors' | 'textSizes' | 'fontFamilies' | 'fontWeights', string[]>

Declarations are kept per source, defaulting to 'app'. That is what lets a rebuild replace everything it scanned without touching what your code registered by hand — the build uses the source 'build'.

FunctionDoes
registerSzcnGroupsAdds names, keeping what the source already declared. The default for hand-written CSS.
setSzcnGroupsReplaces the source’s whole declaration. A category left out is cleared.
clearSzcnGroupsDrops one source, or every source when called with no argument.
getSzcnGroupsReads the names currently in effect — what survived the guard rails, sorted and copied. For tests and diagnostics, not a hot path.
import { registerSzcnGroups } from '@csszyx/runtime';
// once, at app startup — not per-render
registerSzcnGroups({ colors: ['brand', 'tag-blue-bg'], fontFamilies: ['display'] });

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 declared in two conflicting categories (both a color and a text size — text-huge becomes unclassifiable) is dropped from both, for as long as both declarations exist. The effective sets are recomputed from every source on each change, so neither guard rail depends on the order things were declared in.

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.

function szDecode(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:

import { has, szDecode } from '@csszyx/runtime';
szDecode('q3'); // 'w-full' on that build; 'q3' where nothing was mangled
const hasWidthClass = (className: string) => has(className, 'w');

has decodes every token through the registry and strips the variant before matching, so md:hover:w-full and its mangled form both count as a width. A hand-rolled check has to do both itself: szDecode returns the whole original token (szDecode('x7') can be 'md:hover:w-full'), and startsWith('w-') on that is false.

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:

import { szcn } from '@csszyx/runtime';
import type { SzsProps } from '@csszyx/types';
function Card({ szsc }: SzsProps<'title' | 'body'>) {
// Slot default + consumer override — szcn resolves conflicts last-wins.
return <h3 className={szcn('font-medium text-base', szsc?.title)} />;
}

Merges multiple SzObjects. Later objects override earlier ones for the same key.

function _szMerge(...objects: SzObject[]): string

Usage:

const baseStyles = { p: 4, bg: 'gray-100', rounded: 'md' };
const activeStyles = { bg: 'blue-500', color: 'white' };
<div className={_szMerge(baseStyles, isActive ? activeStyles : {})} />

Converts a color value to the correct CSS variable format for use in style props or CSS custom properties.

import { __szColorVar } from 'csszyx/lite';
function __szColorVar(value: string): string
InputOutput
'blue-500'var(--color-blue-500)
'#ff0000''#ff0000' (passthrough)
'--my-var'var(--my-var)
'white'var(--color-white)

Usage:

// Dynamic color for style prop (not sz prop)
<div style={{
'--my-accent': __szColorVar(accentColor),
}} />
// Combined with CSS custom property
<svg style={{ fill: __szColorVar('blue-500') }} />

Initializes the CSSzyx runtime. Call once at app startup.

interface RuntimeConfig {
development?: boolean; // Enable dev mode features
strictHydration?: boolean; // Treat hydration warnings as errors
debug?: boolean; // Enable console debug logging
}
function initRuntime(config?: Partial<RuntimeConfig>): void

Usage:

// In your app entry point
import { initRuntime } from '@csszyx/runtime';
initRuntime({
development: process.env.NODE_ENV === 'development',
strictHydration: true,
debug: process.env.NODE_ENV === 'development',
});

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.

Returns the current runtime configuration (read-only copy).

function getRuntimeConfig(): Required<RuntimeConfig>

Returns true if initRuntime() has been called.

function isRuntimeInitialized(): boolean

Creates a type-safe variant factory that returns sz objects. CVA-equivalent for csszyx.

function szv<V extends VariantSchema>(
config: SzvConfig<V>
): (selection?: VariantSelection<V>) => SzObject
import { szv } from '@csszyx/runtime';
// or:
import { szv } from 'csszyx';
const buttonSz = szv({
base: { display: 'inline-flex', items: 'center', rounded: 'md' },
variants: {
variant: {
default: { bg: 'primary', text: 'primary-foreground' },
outline: { border: true, borderColor: 'blue-500' },
},
size: {
sm: { h: 9, px: 3 },
md: { h: 10, px: 4 },
},
},
defaultVariants: { variant: 'default', size: 'md' },
});
<button sz={buttonSz({ variant: 'outline', size: 'sm' })} />

TypeScript infers valid keys/values from the config literal — no manual annotations needed. See the szv() guide for full docs.

Numeric variant keys are fully supported — useful for index-based variants:

const itemSz = szv({
base: { rounded: 'sm', shrink: 0 },
variants: {
idx: {
0: { opacity: 50 },
1: { opacity: 70 },
2: { opacity: 90 },
},
color: {
normal: { bg: '#2dd597' },
reverse: { bg: '#a78bfa' },
},
},
defaultVariants: { idx: 0, color: 'normal' },
});
<div sz={itemSz({ idx: 1, color: 'reverse' })} />
// → "rounded-sm shrink-0 opacity-70 bg-[#a78bfa]"

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.

Pass an array to the sz prop to compose multiple sz objects with conditional items:

// Static items → extracted at build time (zero runtime)
// Conditional items → _szMerge at runtime
<div sz={[
{ display: 'flex', items: 'center', p: 4 },
isActive && { bg: 'blue-500' },
isDisabled && { opacity: 50, cursor: 'not-allowed' },
]} />
// Compose szv() output with extra overrides
<button sz={[
buttonSz({ variant: props.variant }),
isLoading && { opacity: 50, cursor: 'wait' },
]} />

The compiler pre-computes all static objects at build time; only the conditional merge paths use _szMerge at runtime.

splitBox(className, options?) — Nested Element Routing

Section titled “splitBox(className, options?) — Nested Element Routing”

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 }. Nothing is lost and every token keeps its variant prefix: each lands in exactly one bucket, except the timing group (transition-*, duration-*, ease-*, delay-*), which is declared on both because the state that fires a transition can sit on either node.

function splitBox(className: string, options?: {
outer?: BoxSelector[]; // force these onto the outer node
inner?: BoxSelector[]; // force these onto the inner node
fallback?: 'outer' | 'inner'; // unrecognized token → default 'outer'
}): { outer: string; inner: string }
// box-role | category | class-prefix | category+value pair,
// optionally qualified by a property (`text:color`)
The qualified form reads a class token, so it applies to `splitBox`, `classify`, `has`, `pick` and `omit` only; the sz-object twins key by sz prop and take the unqualified forms.
type BoxSelector = string | Readonly<Record<string, string>>

The split follows the box model: outer = border-outward (margin, position, border, sizing, background, shadow, transform, visibility, the clip the box applies to itself — overflow-hidden/overflow-clip — and the pointer: cursor-*, select-*, pointer-events-*); inner = border-inward (padding, scrolling — overflow-auto/overflow-scroll — display, layout, gap, text, paint-inside, divide-*). The class-token → box-role map is generated from the compiler’s property tables, so it never drifts. Every default is overridable.

import { splitBox } from '@csszyx/runtime';
const { outer, inner } = splitBox('m-4 px-2 md:flex');
// outer: "m-4" inner: "px-2 md:flex"
<Frame className={outer}>
<Content className={inner} />
</Frame>

classify / has / pick / omit — Class Toolkit

Section titled “classify / has / pick / omit — Class Toolkit”

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.

function classify(token: string): {
role: 'outer' | 'inner';
category: string;
property?: string; // only for prefixes that span more than one property
} | undefined
function has(classes: string, selector: BoxSelector): boolean
function pick(classes: string, selector: BoxSelector): string
function omit(classes: string, selector: BoxSelector): string

property names the CSS property inside the category for the prefixes that cover several — classify('text-red-500') reports color and classify('text-sm') reports size, both in category text. It is the same classification szcn merges by, so a token name declared in the app’s Tailwind @theme (see Theme group registry) is read the way a built-in one is. The field is absent when the prefix means exactly one property (p-4) and when the value does not confidently name one (border-t-2) — never null, never empty — and it never changes which node splitBox routes a token to.

A selector qualified with a property matches tokens that satisfy both halves, which is how a rule addresses text colours without touching text sizes:

pick('text-red-500 text-sm font-bold', 'text:color'); // 'text-red-500'
pick('font-bold font-sans', 'font:weight'); // 'font-bold'
has('bg-cover', 'outer:size'); // true
import { splitBox, has, _szMerge } from '@csszyx/runtime';
// A frame that clips needs its content to scroll, and a flex child needs
// `min-h-0` before it will shrink below its own content.
const { outer, inner } = splitBox(className);
const dep = has(outer, { overflow: 'hidden' }) ? 'flex-1 min-h-0 overflow-y-auto' : '';
<Frame className={`${outer} flex flex-col min-h-0`}>
<Scroll className={_szMerge(inner, dep)} />
</Frame>

stripVariant(token) / normalizeBase(base) — Token Parsing

Section titled “stripVariant(token) / normalizeBase(base) — Token Parsing”

The two steps the toolkit takes before it classifies a token, exported so a component that needs one of them does not reimplement it.

function stripVariant(token: string): string
function normalizeBase(base: string): string

stripVariant returns the utility after its variant chain, scanning from the right for the last : at bracket depth zero, so a colon inside a value is not a variant boundary:

stripVariant('md:hover:w-4') // 'w-4'
stripVariant('[&:hover]:w-full') // 'w-full'
stripVariant('bg-[url(http://a/b)]') // unchanged — no variant

A token is at the base breakpoint exactly when stripping leaves it unchanged — after decoding, because on a production build a mangled md:w-1/2 is one opaque token with no colon in it, and stripVariant would call it base:

import { has, szDecode, stripVariant } from '@csszyx/runtime';
const isBase = (token: string) => {
const original = szDecode(token);
return stripVariant(original) === original;
};
const baseTokens = className.split(/\s+/).filter(isBase).join(' ');
const hasBaseWidth = has(baseTokens, 'w');

That is the question a component asks when a responsive width must not count as a width everywhere.

normalizeBase removes the important marker (leading or trailing !) and a leading negative sign: normalizeBase('!w-4') and normalizeBase('-mt-4') give w-4 and mt-4. Give it the output of stripVariant — on a token that still carries its variant it strips only what sits at the ends, so md:!w-4 comes back unchanged.

Neither decodes a mangled token — call szDecode first on a production build. has, pick, omit and splitBox do all three steps themselves.

splitBoxSz(sz, options?) — sz-Object Routing

Section titled “splitBoxSz(sz, options?) — sz-Object Routing”

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)).

function splitBoxSz(sz: SzInput, options?: {
outer?: BoxSelector[];
inner?: BoxSelector[];
fallback?: 'outer' | 'inner'; // unrecognized key → default 'outer'
}): { outer: SzObject; inner: SzObject }
import { splitBoxSz } from '@csszyx/runtime';
splitBoxSz({ m: 4, px: 2 });
// → { outer: { m: 4 }, inner: { px: 2 } }
// Variants route by their inner property; arrays flatten; inner wins ties.
splitBoxSz({ gap: 2, hover: { px: 1 }, md: { m: 4 } });
// → { outer: { md: { m: 4 } }, inner: { gap: 2, hover: { px: 1 } } }
// Item utilities are outer by default; force them back onto the content:
splitBoxSz({ grow: 2, self: 'center' }, { inner: ['grow', 'self'] });
// → { outer: {}, inner: { grow: 2, self: 'center' } }

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.

The sz-object forms of the class toolkit — same generated truth, keyed by sz prop instead of class token.

function classifySzKey(key: string): { role: 'outer' | 'inner'; category: string } | undefined
function hasSz(sz: SzInput, selector: BoxSelector): boolean
function pickSz(sz: SzInput, selector: BoxSelector): SzObject
function omitSz(sz: SzInput, selector: BoxSelector): SzObject

stripSzProps(props) — Safe Prop Forwarding

Section titled “stripSzProps(props) — Safe Prop Forwarding”

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.

import { stripSzProps } from '@csszyx/runtime';
function Box({ sz, ...rest }: BoxProps) {
return <div {...stripSzProps(rest)} />;
}

verifyMangleChecksumAsync(expected, map?) — Integrity Check

Section titled “verifyMangleChecksumAsync(expected, map?) — Integrity Check”

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. verifyMangleChecksumAsync recomputes 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.

function verifyMangleChecksumAsync(
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
import { verifyMangleChecksumAsync } from '@csszyx/runtime';
const ok = await verifyMangleChecksumAsync(expectedChecksum);
if (!ok) {
// map missing, corrupted, or changed without updating the checksum
}

For edge/serverless environments that can’t afford the full runtime, the @csszyx/runtime/lite export provides _sz without hydration guards:

import { _sz } from '@csszyx/runtime/lite';

Lite exports: _sz, _sz2, _sz3, _szMerge, __szColorVar. No hydration, no checksums, no recovery.