Box Kite 1.0.0

4 September 2026
What changed in this version, written as it landed. The same notes are the body of the GitHub release.
These notes are for 1.0.0. The documentation on this site is for 2.1.0, the current release — this site documents the newest version only.

4 September 2026 · npm · 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.

Terminal
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 first.

Highlights

  • 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 — @box-kite/core styles a page with no React in it.
  • Server Components — Box renders in a React Server Component with no 'use client', and so do the pre-built components.
  • 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 — dataAttr, ariaAttr, has, not, nth, group, peer, and nine pseudo-elements.
  • Colour — the palette in OKLCH, an opacity modifier on every colour value, gradients as values, shadows and filters that stack.
  • Layout — container queries, logical sides, rtl/ltr, overflow-safe alignment.
  • 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 — twenty SVG components, <Icon> for anybody's icon set, four chart primitives, and the vars prop.
  • 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

wasis
@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:

JSX
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:

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

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.

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.

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

States and selectors

Four new kinds of nesting, each composing with every prop, breakpoint, theme and the others, in either direction. Docs: /variants and /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.

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

JSX
<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 and /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.

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

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.

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.

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.

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

JSX
<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, /icon and /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.

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

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. 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 states the SemVer policy and the release cadence, 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. 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.
  • 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; 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.