Lossless
Every token lands in exactly one bucket. Concatenate outer + inner back
together and you get your input — no token dropped, none duplicated.
You hand a component one className. It renders several nested elements.
Which styles go where?
The margin belongs on the outer frame. The padding belongs on the inner content. The rounded corners and the shadow stay outside; the scroll and the text color go inside. Get it wrong and your padding double-counts, your shadow clips, your border lands on the wrong edge.
splitBox answers that question for you — at the CSS box-model border line —
and it does it with pure string functions: no React, no DOM, no runtime style
engine. The class-token → box-role map is generated from the compiler’s own
property tables, so it can never drift out of sync with what csszyx emits.
import { splitBox } from '@csszyx/runtime';
const { outer, inner } = splitBox('m-4 px-2 md:flex');// outer → "m-4" (margin: belongs on the frame)// inner → "px-2 md:flex" (padding + layout: belong on the content)A flat className is fine when a component is a single <div>. The moment it wraps
its children, the caller’s intent splits in two:
// The caller writes ONE string…<Panel className="m-4 rounded-xl shadow-lg overflow-hidden p-6" />
// …but <Panel> renders TWO elements, and the styles belong to different ones:function Panel({ className, children }) { return ( <div className={/* m-4 rounded-xl shadow-lg overflow-hidden ??? */}> <div className={/* p-6 ??? */}>{children}</div> </div> );}Put everything on the outer node and the padding pushes the rounded clip out of
place. Put everything on the inner node and the margin collapses. A slot recipe
or a cva-style variant can’t fix this — they generate classes, they don’t
re-route a string the caller already built. Only a runtime partition can.
CSS already draws the line for you. Every box property acts on one side of the element’s border — outward (the box’s relationship to its neighbors) or inward (the box’s relationship to its contents).
outer ─ margin · position · sizing · background · border · shadow · transform · visibility ┌──────────────────────── the border line ────────────────────────┐ │ │ │ inner ─ padding · overflow · display · gap · layout · text · interactivity │ ┌──────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ your content │ │ │ │ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘m-4, absolute,
w-full, bg-white, rounded-xl, shadow-lg, rotate-3, invisible.p-6,
overflow-hidden, flex, gap-2, text-sm, cursor-pointer.splitBox walks the className token by token, asks the generated map “which side
of the border does this act on?”, and drops each token in the matching bucket.
splitBox is framework-agnostic — it’s a string in, two strings out. Use it in
React, Vue, Svelte, Solid, an edge function, or plain HTML.
import { splitBox } from '@csszyx/runtime';
function Panel({ className = '', children }) { const { outer, inner } = splitBox(className); return ( <div className={outer}> <div className={inner}>{children}</div> </div> );}
<Panel className="m-4 rounded-xl shadow-lg overflow-hidden p-6" />;// frame → "m-4 rounded-xl shadow-lg"// content → "overflow-hidden p-6"<script setup>import { computed } from 'vue';import { splitBox } from '@csszyx/runtime';const props = defineProps({ class: { type: String, default: '' } });const box = computed(() => splitBox(props.class));</script>
<template> <div :class="box.outer"> <div :class="box.inner"><slot /></div> </div></template><script> import { splitBox } from '@csszyx/runtime'; export let className = ''; $: ({ outer, inner } = splitBox(className));</script>
<div class={outer}> <div class={inner}><slot /></div></div>import { splitBox } from '@csszyx/runtime';
const { outer, inner } = splitBox('m-4 rounded-xl overflow-hidden p-6');frame.className = outer; // "m-4 rounded-xl"content.className = inner; // "overflow-hidden p-6"These are the defaults. Every one of them is overridable (next section).
| Bucket | Categories | Examples |
|---|---|---|
| outer | margin · position · sizing · background · border · shadow · transform · visibility | m-4 -mt-2 absolute inset-0 w-full bg-white rounded-xl border shadow-lg rotate-3 invisible z-10 |
| inner | padding · overflow · display · layout · gap · text · paint-inside · interactivity | p-6 overflow-hidden flex grid gap-2 text-sm underline ring-2 cursor-pointer |
A mixed string sorts itself in one pass:
splitBox('w-full flex bg-white overflow-hidden invisible p-2 m-2');// outer → "w-full bg-white invisible m-2"// inner → "flex overflow-hidden p-2"splitBox classifies the base utility, so the messy parts of a real
className come along for the ride untouched:
// Variant prefixes are preserved; the base is what gets classified.splitBox('md:px-2 hover:shadow-lg');// outer → "hover:shadow-lg" inner → "md:px-2"
// Stacked + arbitrary variants never get split at the wrong colon.splitBox('@max-[600px]:p-4 [&:hover]:m-2 aria-[sort=asc]:flex');// outer → "[&:hover]:m-2"// inner → "@max-[600px]:p-4 aria-[sort=asc]:flex"
// Negative (-) and important (!) markers are seen through.splitBox('-mt-4 px-2! !flex');// outer → "-mt-4" inner → "px-2! !flex"Three guarantees make it safe to drop into a forwarding component:
Lossless
Every token lands in exactly one bucket. Concatenate outer + inner back
together and you get your input — no token dropped, none duplicated.
Predictable fallback
A token csszyx doesn’t own (a third-party or arbitrary class like
[mask-type:luminance]) goes to outer by default. Flip it with
fallback: 'inner'.
Mangle-safe
It reads the compiler’s tables, so it understands the same class vocabulary the compiler emits — variants, important, negative, value-keyed display/position.
The defaults follow the box model, but your design owns the final call. Pass
outer / inner selectors to force a category, a class-prefix, or a precise
{category: value} pair to the side you want. inner wins ties.
// Send backgrounds to the inner node for this component.splitBox('bg-white m-2', { inner: ['bg'] });// outer → "m-2" inner → "bg-white"
// Keep the clip on the frame.splitBox('overflow-hidden p-4', { outer: ['overflow'] });// outer → "overflow-hidden" inner → "p-4"A BoxSelector is whatever is most convenient:
'outer' | 'inner' // a box-role'content' // alias for inner'overflow' | 'bg' | 'text' // a category'px' | 'bg' // a class-prefix (matches px-2, bg-red-500, …){ overflow: 'hidden' } // a category + value pair (most precise)Routing is one job. The harder one is cross-element dependencies: “if the frame clips its overflow, the scroller inside must actually scroll.” That rule is yours — it depends on your component. But to express it, you need to read the classes, and that’s where projects usually hardcode a brittle list of Tailwind strings.
csszyx owns the truth (a class’s box-role + category); you own the rule. Four pure helpers expose the truth — no rule-DSL, no hardcoded vocabulary:
classify(token) // → { role, category } | undefinedhas(classes, selector) // → boolean ("is there an overflow class?")pick(classes, selector) // → string (keep only matching tokens)omit(classes, selector) // → string (drop matching tokens)classify('px-2'); // { role: 'inner', category: 'padding' }classify('m-4'); // { role: 'outer', category: 'margin' }classify('absolute'); // { role: 'outer', category: 'position' }classify('totally-custom');// undefined (not a csszyx utility)They see through variants and markers too: classify('md:px-2') and
classify('px-2!') both report padding.
The clip stays on the frame; the scroller is derived from it — no string
matching, no className.includes('overflow-hidden'):
import { splitBox, has } from '@csszyx/runtime';
function ScrollArea({ className = '', children }) { // Keep overflow on the frame so the rounded clip is correct. const { outer, inner } = splitBox(className, { outer: ['overflow'] });
// YOUR rule: a clipping frame implies a scrolling child. const scroll = has(outer, { overflow: 'hidden' }) ? 'overflow-y-auto' : '';
return ( <div className={outer}> <div className={`${inner} ${scroll}`.trim()}>{children}</div> </div> );}
<ScrollArea className="overflow-hidden rounded-xl p-4 h-64" />;// frame → "overflow-hidden rounded-xl h-64"// content → "p-4 overflow-y-auto"pick and omit round out the kit when you need to move or strip a category:
pick('m-4 p-2 bg-white', 'outer'); // "m-4 bg-white"omit('p-2 overflow-y-auto flex', 'overflow'); // "p-2 flex"splitBoxSzsplitBox partitions a className string. But a component that stays
sz-native — building styles with szv so the compiler
statically safelists every variant — holds an sz object, not a string.
Bridging through a className to reach splitBox throws that auto-safelisting
away.
splitBoxSz partitions the sz object directly, into { outer, inner } sz
objects you hand straight to nested sz props:
import { splitBoxSz } from '@csszyx/runtime';
splitBoxSz({ m: 4, px: 2 });// → { outer: { m: 4 }, inner: { px: 2 } }It routes each key to the same side its emitted class would land on, so
splitBoxSz(x) agrees with splitBox(compile(x)) by construction — a test gates
the parity. Everything splitBox does, the sz version does on keys, not tokens:
splitBoxSz({ gap: 2, hover: { px: 1 }, md: { m: 4 } });// → { outer: { md: { m: 4 } }, inner: { gap: 2, hover: { px: 1 } } }null/false drop), so a
conditional szv composition partitions in one call:splitBoxSz([base, isLoading && { opacity: 50 }]);SplitBoxOptions. Flex-item utilities
(grow/shrink/self/order) default to the inner content; force them onto
the frame when the frame is the flex item:splitBoxSz( { grow: 2, self: 'center', order: 'first' }, { outer: ['grow', 'self', 'order'] },);// → { outer: { grow: 2, self: 'center', order: 'first' }, inner: {} }The toolkit has sz-object twins as well — classifySzKey(key), hasSz,
pickSz, omitSz — reading the same generated truth keyed by sz prop instead of
class token. The whole component stays sz-native end to end:
import { splitBoxSz } from '@csszyx/runtime';
function Box({ sz, children }) { const { outer, inner } = splitBoxSz(sz); return <div sz={outer}><div sz={inner}>{children}</div></div>;}Zero runtime weight
Pure string functions. No React, no DOM, no style engine — tree-shakeable and safe in server components, edge runtimes, and workers.
Framework-agnostic
String in, strings out. The same call works in React, Vue, Svelte, Solid, and vanilla JS.
Generated, never drifts
The box-role map is generated from the compiler’s property tables and gated by a test — it always matches what csszyx emits.
Lossless & predictable
Every token routed exactly once, variants preserved, unknown tokens to a configurable fallback. Safe in a forwarding component.
This is the part of a styling system most libraries leave to you and a regex: turning a flat string into correctly-placed styles across a real component tree. csszyx ships it as a primitive — because it already knows what every class does.
splitBox / splitBoxSz, classify / classifySzKey, and the has / pick
/ omit toolkit (string and sz-object forms).sz objects before they ever
become a string.