---
name: tonic-ui-sx
description: Choose sx, __sx, and composeSx when authoring or reviewing Tonic UI Box-based components, including wrappers and headless-library prop getters. Use for base styles, style precedence, consumer overrides that do not apply, or incoming __sx overrides that are dropped.
---
# Tonic UI: `sx` vs `__sx`
All `docs/` and `packages/` paths are relative to the repository root. Component
paths such as `button/Button.js` are relative to `packages/react/src/`.
Read [composition and verification](references/composition-and-verification.md)
when implementing a wrapper, transition, multi-feature prop getter, or regression
check. Its worked examples and verification cases supplement the core rules below.
## When to Use
Use this skill for component base styles, `useXxxStyle()` hooks, Box style routing,
incoming `__sx` folds, wrappers overriding children, and headless-library prop getters
such as react-table's `getRowProps()` / `getHeaderProps()`. It applies to any package
rendering Tonic UI Box-based components, including external data-grid integrations.
Also use it to investigate dropped wrapper styles or consumer overrides that lose.
## Current Integration Status
Full `__sx` integration is not complete in this Tonic UI checkout. The helper, Box
channel, and `useSlot` composition exist, and some components use them; the examples
below describe the target styling convention, not proof that every component already
follows it. Inspect the affected implementation before relying on it, and do not expand
a task into full styling integration without a user request.
## The one rule that governs everything: precedence-by-origin
A component's **base styling always loses to a consumer override**, regardless of which CSS
property each touches. A wrapper's **child-override beats the child's base but still loses to
the end consumer**. To make that true structurally (not by convention), Tonic UI routes
*who wrote the style* to a fixed channel — and the channel decides precedence.
So the question is never "what does this style do?" but **"who is writing it?"**
- **You are the component author, styling your own `Box`** → `__sx` (lowest tier).
- **You are a wrapper, overriding a child component you render** → also `__sx` (folded into the child's incoming `__sx`).
- **You are the consumer (app developer)** → `sx` (highest tier), or style props / pseudo props.
A component **never writes its own styling to `sx`.** `sx` is the consumer's escape hatch.
"Writing it" isn't limited to a component's own render body. A higher-order component wrapping a
child, or a headless-library plugin contributing props via a getter function (e.g. react-table's
`getRowProps()`/`getHeaderProps()`, composed across features by a shallow merge), is just as much
"the component author" for this purpose as a render body is — the same hand-built-style-object,
hand-merged-via-`ensureArray`-or-object-spread failure shows up there too, just one layer removed
from JSX.
## The four channels of `Box`
`Box` concatenates four ordered channels into one Emotion class. For declarations of the
**same property at the same selector specificity**, a **later** channel wins.
| Channel | Who writes it | Tier | Notes |
|---|---|---|---|
| **`__sx`** (base sx) | Library / component internals | 0 — lowest | A component's own base styling. Full sx-object format. Internal: not in public `BoxProps`, never forwarded to the DOM. |
| **Style props** (`px`, `bg`, `color`, …) | Consumer | 1 | Flat layout/appearance props, resolved by the `system` transform. |
| **Pseudo props** (`_hover`, `_active`, `_focusVisible`, …) | Consumer | 3 | Each a single prop carrying a style object. |
| **`sx`** | Consumer | 4 — highest | The consumer's arbitrary-style escape hatch. |
`__sx` and `sx` accept the **identical authoring format**: flat declarations, nested selectors
(`'& svg'`, `'&:hover'`), pseudo shortcuts (`_hover`), `theme => …` functions, design tokens,
responsive arrays. They differ only in **tier** (who they belong to), not in what they can express.
## Why `__sx` exists (the problem it solves)
Before `__sx`, base styling was authored as style props (tier 1) + pseudo props (tier 3).
That made precedence a function of *which kind of prop* a value is — not who wrote it. Two
failures followed:
- A component that needed **nested selectors or pseudo-elements** (which flat style props
can't express) had to author base styling via `sx` — but then the component's `sx` outranked
the **consumer's** style props *and* `sx` (single shared slot), so the consumer could no
longer override. Backwards.
- Wrappers overriding a child had to squat on the consumer's `sx` channel and hand-merge the
consumer's `sx` back on top (`sx={[ownSx, ...ensureArray(sx)]}`) — done inconsistently across
the codebase, silently dropping overrides when an author forgot the spread.
`__sx` is a dedicated lowest-priority channel that reuses the `sx` transform. Base styling goes
there; consumer style props and `sx` sit above it and override it. Precedence-by-kind becomes
precedence-by-origin, with no per-author merge bets.
## How to author base styling: the uniform fold
Every base-styling component follows the **same** call site. You cannot predict which
components a future wrapper will inject `__sx` into, so apply this uniformly — don't pick and
choose:
```js
import { composeSx } from '@tonic-ui/utils/internal';
const MyComponent = forwardRef((inProps, ref) => {
const {
// ...component props...
__sx: __sxProp, // destructure incoming __sx OUT of rest
...rest
} = useDefaultProps({ props: inProps, name: 'MyComponent' });
const styleProps = useMyComponentStyle(/* state */); // the COMPLETE base: flat + pseudo + nested
return (
);
});
```
Why each part matters:
- **`__sx` is destructured out of `rest`** so it is not applied twice (once via the spread,
once via the explicit prop).
- **`composeSx(styleProps, __sxProp)` is placed *after* `{...rest}`** so the explicit `__sx`
wins the spread, and the incoming `__sx` (already folded) lands after the component's own
base → an injected override beats the component's base, while the consumer's `sx` (tier 4)
still beats everything.
- **`useXxxStyle()` returns the component's complete base** — flat layout, pseudo rules, and
nested selectors in one value (an object, or an array when conditional/order-sensitive parts
must compose). There is no separate `get*Sx` function; it was folded into `useXxxStyle`.
**Destructure `__sx: __sxProp` (and `...rest`) directly from `useDefaultProps` — in one line.**
Do not capture the whole bag as `const props = useDefaultProps(...)`. You need two things the
opaque bag can't give you cleanly: the incoming `__sx` *pulled out* (so it isn't applied twice)
and a `...rest` that no longer contains it (so the explicit `__sx={composeSx(...)}` after the
spread wins). The one-line destructure is the canonical form:
```js
// ✅ canonical — destructure inline
const { __sx: __sxProp, ...rest } = useDefaultProps({ props: inProps, name: 'X' });
const styleProps = useXxxStyle(...);
return ;
// ❌ opaque bag — can't fold cleanly; `{...props}` carries an unfolded `__sx`,
// and adding `__sx={...}` alongside it double-applies or collides
const props = useDefaultProps({ props: inProps, name: 'X' });
return ;
// ❌ two-step — works, but re-destructuring from a captured bag is noise; inline it
const props = useDefaultProps({ props: inProps, name: 'X' });
const { __sx: __sxProp, ...rest } = props;
```
## `composeSx`: array composition, never object merge
```js
const composeSx = (...values) => values.flatMap((value) => ensureArray(value));
// import { composeSx } from '@tonic-ui/utils/internal'; (internal — not the public barrel)
```
`composeSx` is **variadic** — pass any number of sx-values (`composeSx(base, a, b)`); arrays are
flattened and `undefined` is skipped. It returns an **array** for the `__sx`/`sx` prop.
**Do not spread it as props.**
**If you author the element through `useSlot`, you don't fold `__sx` yourself.** `useSlot`
composes both `ref` (via `useMergeRefs`) and `__sx` (via `composeSx`) across `props` and
`slotProps` internally — put the base in `props.__sx`, pass the consumer's slot props as
`slotProps`, and the base stays below the slot's `__sx`. Do **not** strip `__sx` out of the
slot props or hand-merge it; that's the hook's job (mirrors how it merges `ref`):
```js
const [Slot, slotProps] = useSlot({
props: { ref: combinedRef, __sx: baseStyle }, // base — useSlot keeps it below slotProps.__sx
slot: slots.x ?? Default,
slotProps: resolvedSlotProps, // consumer ref + __sx merged by useSlot
});
```
This is the single most important pitfall. Combining two sx-objects must use **array
composition** (`[a, b]`), not object merge (`{...a, ...b}`):
- **Array** → both objects are emitted; source order resolves conflicts **per declaration**.
An incoming `&:hover` *partially* overrides the base `&:hover` (changes `color`, keeps the
base `background`).
- **Object merge** → a nested key like `&:hover` from `b` **replaces** `a`'s `&:hover`
entirely. The base hover styling is discarded. This is the wrong behavior and a common bug.
When there is **no incoming value to fold** (e.g. a sub-element whose style has no consumer
`__sx` slot), pass the style object **directly** — `composeSx` adds nothing:
```js
// Folding an incoming __sx → use composeSx:
// No incoming __sx → pass directly:
return { ref, onMouseDown, __sx: trackStyleProps };
```
## Transition and Prop-Getter Invariants
- Animation state derived from an enum and static configuration belongs in `__sx`.
Fold incoming `__sx` last in both Box and function-child render paths.
- Function children must spread the handoff onto a Box-based element. Keep the
caller's `style` separate and explicitly destructured as an animation input.
- DOM-measured values belong in inline `style`; conditionally include measured keys
so an undefined measurement does not erase the caller's value.
- Each prop-getter composition layer must fold multiple contributors' `__sx` via
`composeSx`, not shallow object spread.
- Extract base styling into `styles.js` / `useStyle()`, including static
values. Read the linked reference for the worked examples.
## Consumer usage — `sx`
App developers override component styling with `sx` (highest tier), or with style props /
pseudo props. They never touch `__sx` (it's internal and off the public type):
```jsx
{/* style prop, tier 1 — also beats base __sx */}
```
Both remain fully functional, coequal tiers — this doesn't change. Going forward, `sx`
is the **preferred** authoring convention for new/migrated consumer JSX, including on
the layout primitives (`Box`/`Flex`/`Grid`/`Stack`/`StackItem`/`Space`). The
`style-props-to-sx` codemod (`packages/codemod/src/style-props-to-sx`) mechanically
migrates existing flat-prop usage to `sx`.
## The specificity boundary (read before promising "consumers can override anything")
Channel order resolves ties **only at equal selector specificity**. Specificity still applies:
- A **flat** base declaration in `__sx` (e.g. `{ color }`) is overridable by a consumer flat
style prop *or* `sx`.
- A **nested/pseudo** base rule in `__sx` (e.g. `{ '&:hover': { color } }`, `{ '& svg': {…} }`)
has higher specificity than a consumer's **flat** style prop, so a flat style prop will **not**
override it — only a consumer rule of equal-or-higher specificity (a matching `sx` / `_hover`)
will. This is inherent to CSS, not a quirk of `__sx`. Don't tell a consumer a flat `color`
prop will override a base `&:hover` color.
## Naming conventions
- **Hook:** `useStyle` (e.g. `useButtonStyle`); for a multi-part component, the root
is `useRootStyle` (e.g. `useScrollbarRootStyle`, `useCircularProgressRootStyle`).
- **Local variable** holding the hook result: `<...>StyleProps`
(e.g. `styleProps`, `rootStyleProps`, `circularProgressRootStyleProps`).
- **Incoming consumer `__sx`:** destructured as `__sx: __sxProp` (mirrors the `sx: sxProp`
convention).
- **Incoming consumer `style`,** when it needs merging with another style source (a colliding
`<...>StyleProps` hook result, a transition handoff's `transitionStyle`, a measured value):
see the merge pattern in the linked composition reference for the canonical shape
(`const style = { ...styleProp, ...(condition && { key: value }) };`) and the naming-collision
caveat — rename the destructure to `styleProp` only when a colliding `styleProps` exists in the
same scope, never rename the hook result to make room for it.
- The helper is **`composeSx`** — named for what it does, matching the rule below: array
composition, never object merge.
## Red flags when writing or reviewing
- A component writing its **own** styling to `sx=` → should be `__sx`. (`sx` is consumer-only.)
- `__sx={{...a, ...b}}` or `sx={{...ownSx, ...props.sx}}` (object merge of sx-objects) → use
`composeSx(a, b)` (array). Object-merging discards whole nested keys like `&:hover`.
- `composeSx(...)` spread as props (``) → it returns an **array** for the
`__sx`/`sx` prop, not a props object.
- Incoming `__sx` not destructured out of `rest`, or `composeSx(...)` placed **before** `{...rest}`
→ the fold is wrong; an injected override is double-applied or silently dropped.
- `const props = useDefaultProps(...)` capturing the whole bag, then `{...props}` onto the child
→ the incoming `__sx` rides along unfolded; destructure `const { __sx: __sxProp, ...rest } =
useDefaultProps(...)` inline instead (don't capture `props` then re-destructure either).
- A wrapper overriding a child via `sx={[ownSx, ...sx]}` (hand-merge) → migrate to `__sx`; the
child folds it natively, no hand-merge.
- `composeSx(styleProps)` with a single arg where there's genuinely no incoming `__sx` → just pass
`__sx={styleProps}` directly.
- "Consumer's flat style prop should override the base hover" → check specificity; a nested base
rule isn't beaten by a flat prop.
- A transition component spreading its animation state (`opacity`/`transform`/`transition` derived
from the state enum) as flat `Box` props, or handing it to a function child inside `style` →
both belong in `__sx` (see the transition section above).
- A DOM-measured value (content height, auto-computed duration) serialized through `__sx`/`sx` →
inline `style`; each distinct measurement would mint an uncollectable stylesheet class.
## Sources of truth (in this repo)
- `docs/adr/2026-06-24-box-internal-sx-base-channel.md` — why `__sx` was introduced and the
specificity boundary.
- `docs/adr/2026-06-29-scrollbar-scrollview-slot-and-useslot-sx-composition.md` — `__sx`
composition through `useSlot` for a slotted default component (`Scrollbar`/`ScrollView`).
- `docs/adr/2026-07-03-transition-style-through-sx-channel.md` — the transition convention:
animation state through `__sx` in both render modes; the function-child handoff carries `__sx`
and requires a `Box`-based child; DOM-measured values stay on inline `style`.
- `packages/react-base/src/box/Box.js` — the four-channel compose chain.
- `packages/utils/src/internal/composeSx.js` — the helper.
- `docs/plans/2026-07-02-sx-internals-migration.md` — the migration plan covering both
`@tonic-ui/react` and the confirmed `@tonic-ui/react-data-grid` instances (e.g.
`DataGridResizeHandle.js`, `DataGridScrollbar.js`, `RowReorder.js`'s `getRowProps()`
contribution); a live worked example of every pattern above outside a component render body.