# Box Kite 1.0.0

_Box Kite 2.1.0 · a markdown copy of https://www.box-kite.dev/releases/1.0.0/_

_4 September 2026 · [npm](https://www.npmjs.com/package/@box-kite/react) · [Compare v3.3.3...v1.0.0](https://github.com/box-kite/box-kite/compare/v3.3.3...v1.0.0)_

**`@cronocode/react-box` is Box Kite now.** Same library, same API, a new name — and the engine is a package of its own. It is also the largest release the library has had: everything that landed between 3.3.3 and the rename ships here, and these notes cover all of it.

```bash
npm install @box-kite/react
```

For most projects the move is one find-and-replace, and `@cronocode/react-box@3.4.0` re-exports this package for anything not ready yet. If you are upgrading from 3.3.3 rather than only renaming, read [Breaking changes](#breaking-changes) first.

## Highlights

- **[The rename](#the-rename)** — `@cronocode/react-box` is `@box-kite/react`, with a compatibility bridge at `@cronocode/react-box@3.4.0`.
- **[The engine is its own package](#the-engine-is-its-own-package)** — `@box-kite/core` styles a page with no React in it.
- **[Server Components](#server-components)** — Box renders in a React Server Component with no `'use client'`, and so do the pre-built components.
- **[Accessibility](#accessibility)** — Tooltip, Dropdown, Select, DataGrid and the form controls follow the APG patterns, and the primitives they are built from are published at `@box-kite/react/a11y`.
- **[States and selectors](#states-and-selectors)** — `dataAttr`, `ariaAttr`, `has`, `not`, `nth`, `group`, `peer`, and nine pseudo-elements.
- **[Colour](#colour)** — the palette in OKLCH, an opacity modifier on every colour value, gradients as values, shadows and filters that stack.
- **[Layout](#layout)** — container queries, logical sides, `rtl`/`ltr`, overflow-safe alignment.
- **[Motion](#motion)** — `animation`, `Box.keyframes()`, springs, `startingStyle`, and `<Presence>` for the way out, all of it stopping under reduced motion on its own.
- **[SVG, icons and charts](#svg-icons-and-charts)** — twenty SVG components, `<Icon>` for anybody's icon set, four chart primitives, and the `vars` prop.
- **[The escape hatch](#the-escape-hatch)** — `css`, a class rather than a style attribute.

212 CSS props, 96 of them new since 3.3.3. Eight new component entry points. Over 1,900 tests, run against React 18 and React 19.

## The rename

| was                          | is                     |
| ---------------------------- | ---------------------- |
| `@cronocode/react-box`       | `@box-kite/react`      |
| `@cronocode/react-box/core`  | `@box-kite/core`       |
| `@cronocode/react-box/types` | `@box-kite/core/types` |

`/rsc`, `/a11y`, `/ssg` and every `/components/*` subpath keep their name — only the prefix changes.

**`@cronocode/react-box@3.4.0` is a compatibility bridge**: every entry re-exports the new package, so an `npm update` cannot break a build that has not migrated. It is deprecated and receives no further changes.

### Three things the bridge cannot forward

**1. Type augmentation.** TypeScript augments the module you name, and a re-export cannot pass that through. If you extend the prop types with `Box.extend()`, change the string by hand — and note that it points at core now, because the types are derived from the prop registry rather than from any binding:

```ts
declare module '@cronocode/react-box/types' { … }   // before
declare module '@box-kite/core/types' { … }         // after
```

**2. Two DOM ids**, both documented debugging landmarks. Update anything selecting them in a test or a global stylesheet:

| was                             | is                             |
| ------------------------------- | ------------------------------ |
| `<style id="crono-styles">`     | `<style id="box-kite-styles">` |
| `#crono-box` (portal container) | `#box-kite-portal`             |

**3. The warning prefix** is `[box-kite]` rather than `[react-box]`. Matters only if you assert on it.

Two smaller ones: the AI context file is `BOX_KITE_AI_CONTEXT.md` (published under both names for one minor cycle), and the skill and rules that ship in the tarball are `.claude/skills/box-kite/` and `.claude/rules/box-kite-rules.md`.

## The engine is its own package

`@box-kite/core` publishes the styling engine with **no React at all**: `createStyleEngine()`, `engine.classNames(props)`, `createThemeController()` for theming with no provider, `getStyles()` for static CSS, and the palette. A plain-DOM app, a Web Component or another framework's binding installs one small package instead of a React library whose peer dependencies npm would install for it.

`@box-kite/react` depends on it at an exact version and imports it rather than carrying a copy, so installing the React package still gets you both — and `getDefaultEngine()` from core returns the very instance the React `Box` uses. `examples/vanilla` is a whole page built on it, and CI proves that neither the engine's sources nor its chunks name `react`.

## Server Components

**Box renders in a React Server Component with no `'use client'`.** The `react-server` export condition resolves to a hook-free Box whose CSS ships as `<style href precedence>` elements React 19 hoists into `<head>`, so a Server Component needs no provider, no configuration and no stylesheet to import. Client trees opt in with `Box.configure({ sink: 'element' })`. In element mode the rules live in `@layer`, so your own unlayered CSS wins over Box props, and `Box.extend()` must run before the first render. React 19 only.

**The pre-built components work there too.** `Flex`, `Grid`, `Button`, `Textbox`, `Textarea`, `RadioButton`, the SVG and chart components and the semantic tags render on the server when a Server Component imports them. `Dropdown`, `Tooltip`, `DataGrid`, `Checkbox`, `Select` and `Form` carry a `'use client'` banner, so importing one from a Server Component opens a client boundary instead of failing the build with `createContext is not a function`. A `DataGrid` also renders on a server now — its column menu used to read `window.innerWidth` during render, so any server render of a grid threw.

**SSR has a real API.** `getStyles()` and `resetStyles()` on `@box-kite/react/ssg` exist for real (they were documented and missing), the server no longer installs a fake `global.document`, and `Box.configure({ sink: 'string' })` collects CSS with no DOM at all. The Next.js App Router example in `examples/next-app` is built and smoke-tested in CI, and the guide on [/server-components](https://www.box-kite.dev/server-components/) is written against it.

## Accessibility

Every component runs through axe on every push, and the pre-built components follow the WAI-ARIA Authoring Practices patterns. Most of this changed markup, which is why several items appear again under [Breaking changes](#breaking-changes).

**The behaviour primitives are published.** `@box-kite/react/a11y` is the set the components are built from, for anyone building their own patterns: `useControllableState` (with change reasons), `useDismiss` (Escape and outside pointer, layered), `useFocusReturn`, `useRovingFocus` (arrows, Home/End, typeahead, DOM focus or `aria-activedescendant`), `useIdentifier`, and a `VisuallyHidden` component. About 3 KB gzipped, with no styling engine behind it. See [a11y primitives](https://github.com/box-kite/box-kite/blob/main/docs/a11y-primitives.md).

**`Tooltip` is the APG tooltip.** It shows on hover _and_ on focus, carries `role="tooltip"`, wires `aria-describedby` onto a trigger you render through a render prop, dismisses on Escape without moving focus, lets the pointer travel onto the bubble, and never hides itself on a timer — WCAG 1.4.13 in full. New: `content`, `open`/`defaultOpen`/`onOpenChange(open, { reason })`, `openDelay` (300 ms, ignored by focus), `closeDelay` (150 ms), and a `tooltip` component style. The positioning half is a component of its own now, `Overlay`: same props, no ARIA, no open state — the thing to reach for if you used `Tooltip` to escape an `overflow: hidden`.

```tsx
<Tooltip content="Saves the draft">{(trigger) => <Button props={trigger}>Save</Button>}</Tooltip>
```

**`Dropdown` and `Select` are the APG select-only combobox.** The trigger is `role="combobox"` with `aria-expanded`, `aria-controls` and `aria-activedescendant`; the popup is a `role="listbox"` of `role="option"` rows. Down/Up/Enter/Space/Home/End and typeahead open and navigate, Enter/Space choose, Escape closes unchanged, Tab chooses and leaves. Pass the new `label` prop, because a combobox is not named by what it contains. In `isSearchable` mode the search `<input>` _is_ the combobox (`aria-autocomplete="list"`), always rendered, with the editable keyboard map: printable keys type, only Down/Up move through the list, and Escape closes before a second Escape clears the field.

**The DataGrid is the APG grid.** The scrolling element carries `role="grid"` with `rowgroup`/`row`/`gridcell` under it and describes the whole grid rather than the window it renders, so `aria-rowcount` and `aria-rowindex` are right under virtualization. One cell is in the tab order at a time: the arrows move it, Home/End go to the row's ends, Ctrl+Home/End to the grid's corners, PageUp/PageDown a screenful, Enter/Space sort a header or step into the cell's own control, F2 steps in and Escape steps out. The column menu is a real menu, the filter-clear controls are real buttons, everything the grid draws for itself is named, and the column resizer is a keyboard-operable `role="separator"` (arrows, Home, End, `aria-valuenow` in pixels). Give the grid a `def.title` — a grid is not named by the rows in it. Known gap: Tab does not yet stay inside the grid.

**The form controls name themselves, and there is a `Switch`.** `Checkbox`, `RadioButton` and the new `Switch` take a `label` prop that renders the wrapping `<label>` itself, so the whole row is a click target and no generated id is needed. `Switch` is `role="switch"` over a real checkbox input, so focus, Space, the disabled state and form submission come from the platform — and Enter toggles too. `Checkbox` reports `aria-checked="mixed"` alongside the `indeterminate` property. `RadioGroup` is the APG radio group: the role, a shared `name`, `onChange(value, { reason })`, and arrow keys that move _and_ select.

**A user's preferences are props, and reduced motion is on by default.** `motionReduce`, `forcedColors` and `contrastMore` are three media keys shaped like a breakpoint. The default is the part to know: every Box transitions on `--transitionTime`, and `prefers-reduced-motion: reduce` now sets it to `0s`, so the library stops animating for a user who asked it to, with nothing to configure. `forcedColors` takes the eight CSS system colours (`Canvas`, `ButtonText`, `Highlight`, …) on every colour prop, because a forced-colours mode keeps nothing else. New pseudo-class prop `focusVisible`, for the ring a keyboard user needs and a mouse user does not.

The measured axe baseline, and what automated checks cannot prove, are published in [a11y testing](https://github.com/box-kite/box-kite/blob/main/docs/a11y-testing.md).

## States and selectors

Four new kinds of nesting, each composing with every prop, breakpoint, theme and the others, in either direction. Docs: [/variants](https://www.box-kite.dev/variants/) and [/pseudo-elements](https://www.box-kite.dev/pseudo-elements/).

**A state your own code sets is a selector.** `dataAttr={{ 'state=open': { … } }}` is `[data-state="open"]` (a bare key is presence), `ariaAttr={{ selected: { … } }}` is `[aria-selected="true"]`, `has={{ ':checked': { … } }}` is `:has(:checked)`, `not={{ hover: { … } }}` is `:not(:hover)`. The record key becomes the selector, so it is validated first: a key the grammar rejects drops its whole block, the way an unmatched value does.

```tsx
<Box props={{ 'data-state': state }} dataAttr={{ 'state=busy': { bgColor: 'amber-500' } }} />
```

**Somebody else's state is `group` or `peer`.** `group={{ 'card/hover': { opacity: 1 } }}` styles an element from an ancestor's state (`.card:hover .x`), and `peer={{ 'agree/checked': { … } }}` from a preceding sibling's (`.agree:checked ~ .x`). The state half is any pseudo-class, or a `data-`/`aria-` prefix for an attribute the ancestor carries. `hoverGroup` and its four siblings still work, compile to the same rule, and are deprecated in favour of `group`.

**A position among siblings is a variant.** `nth={{ odd: { … }, 'last 1': { … } }}` is `:nth-child(odd)` and `:nth-last-child(1)`; it also takes `first`, `last`, `only`, `even` and an `An+B` formula.

**Eight more browser states are pseudo-class props**: `open`, `placeholderShown`, `autofill`, `inRange`/`outOfRange`, `target`, `inert`, and `visited` (colour properties only, because the browser refuses the rest). `pointerCoarse`/`pointerFine` are two more media keys, ranked below the preferences.

**A pseudo-element is a prop, and `content` quotes itself.** `before`, `after`, `placeholder`, `selection`, `marker`, `firstLine`, `firstLetter`, `backdrop`, `fileButton`. `before`/`after` come with `content: ''`, because a generated element with none renders nothing at all — and nesting one under `hover` gives you a pseudo-element that appears on hover. `content` takes text and quotes it (`content="New"`), or CSS you wrote (`attr(data-count)`, `'"Step " counter(step)'`). CSS allows one pseudo-element per selector, so it is a slot: a second nested one is a type error. `marker` and `selection` reach descendants, so they go on the `<Ul>` or the `<P>`.

```tsx
<Ul marker={{ color: 'indigo-500' }}>
  <Li after={{ content: 'beta', fontSize: 10, ms: 2, color: 'sky-500' }}>Container queries</Li>
</Ul>
```

Also new here: `Ul`, `Ol` and `Li` in `components/semantics`, and `data-*` attributes typecheck in the `props` bag of `Box` and every pre-built component.

## Colour

Docs: [/colors](https://www.box-kite.dev/colors/) and [/gradients-shadows](https://www.box-kite.dev/gradients-shadows/).

**The palette is OKLCH now.** Twenty-six families of eleven steps, 286 tokens; `mauve`, `mist`, `olive` and `taupe` are new. No token name changed, but the values did — a perceptual space keeps a step even across the hues, so the vivid 300–600 steps of green, orange, sky, lime, emerald and teal shift visibly (`green-400` was `#4ade80`; it is `#05df72`).

**A colour carries its own opacity.** `bgColor="blue-500/40"` works on every colour prop, on a `vars` entry, and on a variable of your own from `Box.extend()`. It compiles to `color-mix(in oklab, var(--blue-500) 40%, transparent)`, so the token stays a variable: still themed, still one shared class. Not `opacity`, which fades the element and everything in it. A token the palette does not have, or a percentage outside 0–100, emits no rule and no class name.

**A gradient is a value.** `bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }}` — the key names the kind (`linear` a direction or an angle, `radial` a shape, `conic` a start angle), `colors` are the stops, and a stop is any colour value, a `token/alpha`, a `var()`, or a `[colour, position]` pair. `interpolate: 'oklch'` keeps two stops out of the grey middle sRGB drags them through, and `oklch-longer` turns two stops into a spectrum. The record is judged whole: one bad stop or one unknown key and it emits nothing. `maskImage` takes the same record and reads its alpha, so an edge fade is a gradient to `transparent`; `bgClip="text"` beside `color="transparent"` is how a gradient becomes type.

**Four shadows stack** rather than overwriting one `box-shadow`: `shadow` (`xxs`…`xxl` beside the original `small`/`medium`/`large`), `insetShadow`, and `ring`/`insetRing` — a width in pixels that follows `borderRadius` and costs no layout. Each takes a colour of its own (`shadowColor`, `ringColor`, …), `none` clears one layer, and `textShadow` arrives with `textShadowColor`.

**Nine filters stack too.** `blur`, `brightness`, `contrast`, `grayscale`, `hueRotate`, `invert`, `saturate`, `sepia` and `dropShadow` each set one layer of a composed `filter`, so `hover={{ brightness: 110 }}` keeps the blur. A number is the function's own unit — `brightness={110}` is a percentage, `hueRotate={90}` degrees, `blur={3}` pixels — and `blur` also takes the `xs`…`xxxl` scale. The nine `backdrop*` twins are the same functions behind the element: `backdropBlur="sm" backdropSaturate={180} bgColor="white/20"` is glassmorphism. `dropShadow` is cast by the shape, where `shadow` draws a rectangle.

```tsx
<Box bgGradient={{ linear: 'tr', colors: ['sky-500', 'indigo-600'] }} shadow="lg" ring={1} ringColor="white/20" borderRadius={3} p={6} />
```

## Layout

**A component that answers to its own width, not the window's.** `container` makes an element a query container (`true`, or a name), and `cq={{ md: { d: 'row' } }}` is `@container (min-width: 28rem)`. Six sizes, `xs` 20rem through `xxl` 42rem, each with a complement (`maxMd` is `not (min-width: 28rem)`, so the two can never both match), and `cq={{ 'sidebar/md': { … } }}` addresses a container by name. A container query ranks after every breakpoint and before every preference. Docs: [/container-queries](https://www.box-kite.dev/container-queries/).

**A side is logical, and the direction is a state.** `ps`/`pe`, `ms`/`me`, `bs`/`be`, `insetStart`/`insetEnd`, `borderRadiusStart`/`borderRadiusEnd` and the four logical corners (`borderRadiusStartStart`…, block axis first) are the two sides of the inline axis that `px`, `mx` and `insetX` already worked on. Start is left in a left-to-right reading and right in a right-to-left one, resolved by the browser from `dir`, so a translation needs no second stylesheet and nothing re-renders; `textAlign` takes `'start'`/`'end'`. For what a logical property cannot express, `rtl={{ flip: 'xAxis' }}` and `ltr={{ … }}` are pseudo-class keys compiling to `:dir(rtl)`/`:dir(ltr)` — and `ltr` matches a document with no `dir` at all, because left-to-right is the initial value. The components follow: the arrow keys follow the reading order, a DataGrid column pins to `'START'`/`'END'`, and a portalled popup carries its direction out of the tree with it. Docs: [/rtl](https://www.box-kite.dev/rtl/).

**An alignment can be overflow-safe, and a ratio is a value.** `'safe center'` and its five siblings are values on all eight alignment props (`justifyItems` and `placeItems` are new); `safe center` centres until the content stops fitting and then aligns to `start`, where plain `center` overflows both edges. `aspectRatio` takes `'square'`, `'video'`, `'4/3'` or a number. `insetX`/`insetY` are `inset-inline`/`inset-block`, and every inset prop takes `'auto'` and the fractions. The parts of a native control the page does not draw are props too: `accentColor`, `caretColor`, `colorScheme`, `fieldSizing="content"`, `scrollbarGutter="stable"`, and `willChange`. `display` gains `inline` and the five table values.

## Motion

Docs: [/animation](https://www.box-kite.dev/animation/).

**An animation is a prop.** `animation="spin"` (or `pulse`, `bounce`, `ping`) needs no registration and stops on its own under `prefers-reduced-motion`, because a preset's duration is a multiple of `--transitionTime`. `Box.keyframes({ 'slide-in': { from: { opacity: 0, translateY: 3 }, to: { opacity: 1, translateY: 0 } } })` registers your own; the steps are Box props, and a sequence is written the first time a rule names it, so it reaches `getStyles()` and a Server Component can animate with no client JavaScript. New longhands: `animationName`, `animationDuration`, `animationDelay`, `animationIterationCount`, `animationDirection`, `animationFillMode`, `animationPlayState`, `animationTimingFunction` and `transitionDelay`. **Times are milliseconds** (`animationDuration={1100}`). `transition` takes a property group (`colors`, `opacity`, `shadow`, `transform`, `size`, `filter`), and `Box.configure({ transition: 'colors' })` — or `false` — narrows or removes the `transition: all` every Box gets.

**The transforms compose.** `translateX`, `translateY`, `rotate` and `scale` are CSS longhands now, so they no longer overwrite one `transform`. `scale={1.05}` is new.

**A spring is a value.** `spring`, `spring-gentle`, `spring-bouncy` and `spring-snappy` are values on both timing-function props and — because a spring is a curve _and_ a settling time — on both duration props. They are damped oscillators sampled into `linear()`, so a spring costs no runtime at all, and `Box.spring({ stiffness, damping, mass, velocity })` returns `{ easing, duration }` for one of your own. Every `linear()` carries an `ease-out` under it for the browsers that lack it.

```tsx
<Box transition="transform" transitionTimingFunction="spring-bouncy" transitionDuration="spring-bouncy" hover={{ scale: 1.1 }} />
```

**An entrance is a prop.** `startingStyle={{ opacity: 0, translateY: 2 }}` is what a just-mounted element starts from — `@starting-style` wrapped around the finished rule. Plain props only; everything else nests around it. `transitionBehavior="allow-discrete"` lets `display` transition, flipping at the end, so an element that hides rather than unmounting animates out as well as in; `interpolateSize="allow-keywords"` is what makes `height: auto` animate (Chromium-only, snaps elsewhere). `Tooltip` and the `Dropdown` popup carry one.

**An exit is a component.** React removes a node before its styles can change, so `<Presence present>` keeps rendering its child with `present: false` until the child's own computed `transition-duration` says the transition is over. It is a render prop, given `{ present, ref, props }`, where `ref` goes on the element carrying the transition. Under reduced motion the measured wait is zero and the node leaves in the same commit.

```tsx
<Presence present={open}>
  {({ present, ref, props }) => (
    <Box
      ref={ref}
      props={props}
      opacity={present ? 1 : 0}
      translateY={present ? 0 : -2}
      startingStyle={{ opacity: 0, translateY: -2 }}
      transitionDuration={320}
    />
  )}
</Presence>
```

## SVG, icons and charts

Docs: [/svg](https://www.box-kite.dev/svg/), [/icon](https://www.box-kite.dev/icon/) and [/charts](https://www.box-kite.dev/charts/).

**Every SVG element is a component.** `@box-kite/react/components/svg` has `Svg`, `G`, `Defs`, `Path`, `Circle`, `Ellipse`, `Rect`, `Line`, `Polyline`, `Polygon`, `SvgText`, `TSpan`, `LinearGradient`, `RadialGradient`, `Stop`, `ClipPath`, `Mask`, `Use`, `SvgSymbol` and `Marker` — each a Box, each rendering on a server. Each settles the names SVG and Box both use, for its own element: on `Path`, `d` is path data; on `Rect`, `width={40}` is forty user units; on `SvgText`, `x`/`y` are the attributes; on `Circle`, `cx`/`cy`/`r` are the CSS props that transition. `Svg` names itself — no `label` means `aria-hidden`, a `label` means `role="img"`. `BaseSvg` is deprecated in favour of `Svg` and `Icon`.

**Twenty-one more SVG properties are Box props**, beside `fill`, `stroke` and the new `clipPath`: `fillOpacity`, `fillRule`, `strokeOpacity`, `strokeWidth`, `strokeLinecap`, `strokeLinejoin`, `strokeMiterlimit`, `strokeDasharray`, `strokeDashoffset`, `paintOrder`, `vectorEffect`, `shapeRendering`, `textAnchor`, `dominantBaseline`, and the geometry `cx`, `cy`, `r`, `rx`, `ry`, `x`, `y`. The lengths are SVG user units — no divider, no unit — and geometry is CSS, so it transitions: a gauge fills on hover with no JavaScript. A paint server is a value, too: `fill="url(#sky)"`, `stroke="var(--chart-1)"`, `clipPath="url(#frame)"`, so a gradient fill is themed and hoverable.

```tsx
<Circle
  cx={48}
  cy={48}
  r={38}
  stroke="sky-500"
  strokeWidth={6}
  strokeDasharray={239}
  strokeDashoffset={239}
  hover={{ strokeDashoffset: 60 }}
/>
```

**An icon from anybody's set takes Box props.** `<Icon size={5} color="amber-400" hover={{ color: 'amber-300' }} label="Sunny"><Sun /></Icon>` wraps one element from lucide, Tabler, react-icons, or an `<svg>` you paste in, and hands it nothing: the props become a class on the icon's own `<svg>`, where CSS outranks the `width`/`height` attributes the set writes. `size` is the ÷4 scale — an icon set's own `size` is pixels, so `size={20}` becomes `size={5}`. Any Iconify icon comes through the same component, at build time with `unplugin-icons` or at runtime with `@iconify/react`. The hook behind it, `useClassNames`, is exported for any element this library cannot render: a `motion.div`, a router's `NavLink`, a third-party chart.

**Four dashboard shapes take Box props.** `Sparkline`, `ProgressRing`, `Gauge` and `MiniDonut` in `@box-kite/react/components/chart`, server-safe and built from the SVG components. Not a chart library — no axes, no legends. A shape is an attribute, so ten thousand sparklines generate no CSS; paint is a class, so the rows share one rule; a ring's fill transitions.

**A CSS variable is a prop, and a Recharts chart stops naming colours.** `vars={{ 'color-revenue': 'sky-500', 'chart-gap': '4px' }}` declares `--color-revenue: var(--sky-500)` and `--chart-gap: 4px` on the element and everything inside it — including markup this library never rendered. It is an ordinary prop, so it nests in `theme`, `hover` and a breakpoint, and it lands in a class. `<ChartContainer series={['revenue', 'cost']}>` declares `--chart-1`…`--chart-6` in both themes plus one `--color-<series>` per series — the names the shadcn charts ecosystem already uses — so `<Line stroke="var(--color-revenue)" />` is all a chart ever says about paint, and it flips light to dark with its own code untouched.

## The escape hatch

`css={{ mixBlendMode: 'multiply', WebkitLineClamp: 2 }}` takes a style object for the properties the library has no prop for and compiles it through the same pipeline as every other prop: one shared class, nested anywhere a prop nests, rendered on a server, and never a `style` attribute. Names are camelCase and typed by csstype, so a misspelt property is a compile error; values are CSS written as they stand, so a length wants its unit, and a colour token resolves the way a `vars` value does. It sorts last, so on one element it wins the property a typed prop also names. Three ways out, in order: a typed prop, `Box.extend()` for anything used twice, `css` for the one-off. Docs: [/escape-hatch](https://www.box-kite.dev/escape-hatch/).

## The engine

**`createStyleEngine()`.** All engine state lives on an instance — the class-name cache, the rule registry, the variables, the prop and component registries. Every public API delegates to a lazily created default, and a test or a second root can build its own.

**`Box.configure()`.** `classNames: 'hashed' | 'readable' | 'stable'`, `sink: 'cssom' | 'textContent' | 'string' | 'element'`, and `transition`. The sink defaults to the environment. Readable class names are no longer switched on by `NODE_ENV=test`; a test setup asks for them.

**Styles are written from `useInsertionEffect`**, ahead of every layout effect in the commit, so a component that measures layout in `useLayoutEffect` sees the CSS of the whole commit. Rules also flush on their own microtask, so `Box.getVariableValue()` outside a render gets its `:root` declaration.

**`Box.extend()` and `Box.components()` accumulate** across calls instead of dropping earlier registrations; a variable declared with `Box.extend({ variables })` is usable on every colour, background-image and shadow prop without `extendedPropTypes`; a value that matches no definition emits no dangling class name; and breakpoint rules are written `@media (min-width: …)` with the space, so happy-dom and jsdom stop dropping them.

**`src/core` is React-free, and CI proves it.** The engine imports no React — enforced by ESLint and a module-graph check — and the React binding is about a tenth of the source.

**Hover shows the divider.** Every prop carries a TSDoc `@example` measured from the engine (`p={4} → padding: 1rem`, `fontSize={14} → font-size: 0.875rem`) that travels into the published types, and `api/props.json` is the same measurement as data. A CI check fails when a divider changes and the reference beside it does not.

## Docs and trust

- **box-kite.dev.** The docs live at [www.box-kite.dev](https://www.box-kite.dev). Every route is prerendered HTML on the library's own `getStyles()`, so a page paints with no JavaScript, and the site's script went from 1.22 MB gzipped to 118 KB.
- **The snippets compile.** Every hand-written code block on the site is compiled against the published entry points in CI, and the live demos are the page's own JSX.
- **New pages**: server components, SVG, icons, charts, variants, pseudo-elements, container queries, RTL, animation, gradients and shadows, the escape hatch, overlay, and switch.
- **Stewardship.** [SUPPORT.md](https://github.com/box-kite/box-kite/blob/main/SUPPORT.md) states the SemVer policy and the release cadence, [SECURITY.md](https://github.com/box-kite/box-kite/blob/main/SECURITY.md) how to report privately. Tests run against React 18 and React 19, coverage is enforced on the engine, and size budgets on every entry — with `publint` and `arethetypeswrong` — are enforced in CI.

## Breaking changes

The rename itself is [above](#the-rename). Beyond it, in the order you are most likely to meet them:

- **`Tooltip` takes a render prop for its trigger** and shows on focus as well as hover: `<Tooltip content="…">{(trigger) => <Button props={trigger}>…</Button>}</Tooltip>`. If you used `Tooltip` only to escape an `overflow: hidden`, that is `Overlay` now.
- **`Dropdown` and `Select` markup changed.** The trigger is `role="combobox"` (`getByRole('button')` becomes `getByRole('combobox')`), the popup is a listbox of options, `showCheckbox` boxes are decoration and no longer appear as checkboxes, the trigger is `type="button"` so it no longer submits a surrounding form, and it wants a `label`. Delete any hand-written `role="combobox"` or `role="option"`. In `isSearchable` mode, `getByRole('textbox')` becomes `getByRole('combobox')` and `props` land on the field rather than on a button around it.
- **The DataGrid markup changed.** `getByRole('presentation')` becomes `getByRole('grid')`; F2 on a header cell lands on the resizer rather than the column menu; and the pinned-edge variant keys were renamed (`isFirstLeftPinned` is `isFirstStartPinned`, and the three others likewise), so a `Box.components()` override reaching into the grid's internals needs the new names.
- **Reduced motion is respected by default.** `prefers-reduced-motion: reduce` zeroes `--transitionTime`, so transitions, the `Switch` thumb and the DataGrid loading bar stop for those users. `motionReduce={{ … }}` is how you opt back in.
- **The palette values changed** to their OKLCH equivalents; no token name changed. Check brand colours against the swatches on [/colors](https://www.box-kite.dev/colors/).
- **`translateX`/`translateY` write the `translate` longhand**, not `transform`, so they compose with `rotate` and `scale`. Anything that relied on `transform` being the property, or on translate applying after a rotation, changes. `flip` and `scale` both write `scale`: use one.
- **Readable class names in tests are opt-in.** A `NODE_ENV=test` run no longer gets them implicitly; call `Box.configure({ classNames: 'readable', sink: 'textContent' })` in your test setup.
- **`Box.extend()` and `Box.components()` accumulate.** A second call used to replace the first; now it adds to it. `clean` is still how you drop a block on purpose.
- **`BaseSvg` is deprecated.** It is `Svg` plus the 24×24 preset now, and a `BaseSvg` with no `label` is `aria-hidden` where it used to say nothing. Move to `<Svg viewBox="0 0 24 24" width="1.5rem">`, or to `<Icon>` if it held somebody else's icon.
- **On `Textbox` and `Textarea`, a `placeholder` object is styles** and a string is still the attribute; with both, the text goes in `props`. `placeholderStyles` is the old name and still works.
- **`hoverGroup`, `focusGroup`, `activeGroup`, `disabledGroup` and `selectedGroup` are deprecated** in favour of `group`; they still work and share its class. A group name and a theme name must be CSS identifiers now, and `Tooltip` no longer takes an `open` style prop — its own `open` state prop wins the name.
- **`component="orders-datagrid"` is gone.** It existed for a docs demo and should never have shipped.
- **`focusWithin` never existed** and is no longer listed. `focus` compiles to `:focus-within`, and the new `focusVisible` is `:focus-visible`.
- **`@box-kite/react/types` does not exist.** The augmentation target is `@box-kite/core/types`.

## Fixes

Notable fixes since 3.3.3, beyond the ones folded into the sections above:

- **A pseudo-class nested inside a group was applied to the group.** `hoverGroup={{ card: { hover: … } }}` emitted `.card:hover:hover .x`; the element's own state stays on the element now.
- **A pseudo-element nested in a state produced an invalid selector.** `checked: { before: … }` generated `::before:checked`, which the browser dropped whole — the `Switch` thumb never moved. The element is always last now.
- **A `@keyframes` step moving `translateX`/`translateY` jumped** instead of moving, because an unregistered custom property animates discretely; both axes are registered `@property` now.
- **An `@starting-style` rule lost to the value it started from** whenever a theme or a variant raised the specificity, so the entrance silently never ran. Starting declarations are emitted `!important`, which reaches nothing but the before-change style.
- **Importing the library allocated about 1.5 GB.** A table of every subset of the pseudo-class keys was built at module scope; it is a bitmask now, and the import takes a few milliseconds.
- **`Box.configure({ transition: 'colors' })` transitioned one property out of seven**, because a property group is a list and the shorthand gave its duration to the last item. Written as longhands now.
- **The dropdown popup animated the wrong way when it opened upward.** A popup now grows away from its trigger and collapses back into it, in both directions.
- **A popup portalled out of a right-to-left subtree read left to right**, and the arrow keys ran the wrong way in a right-to-left list. Both fixed.
- **Thirty string-valued props accepted anything** (`width="banana"` reached CSS verbatim); they validate now, and `strokeDasharray` with them. Negative fractions work on the margin props (`mt="-1/4"`).
- **A bare `outline` width drew nothing** outside a focus state, because `outline-style` starts at `none`; the prop declares `solid` beside the width.
- **A conditional `undefined` erased a component style's block** in `mergeDeep`; it is skipped now.
- **A `Dropdown` trigger lost 20px of height** when it had nothing to display.
- **`::marker` and `::selection` reach the descendants** that actually draw them.
- **`vectorEffect` and `dominantBaseline` reach descendants**, and the base rule that animates shapes inside an `Svg` names every shape, not four of them.

## Upgrading

1. `npm uninstall @cronocode/react-box && npm install @box-kite/react`, then replace the specifier across your source.
2. If you augment the prop types, point the `declare module` at `@box-kite/core/types`.
3. Search your tests and global CSS for `crono-styles`, `crono-box` and `[react-box]`.
4. Walk the [Breaking changes](#breaking-changes); the component ones show up first as failing `getByRole` queries.
5. If you need time, `@cronocode/react-box@3.4.0` re-exports the new package unchanged.

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

