# Box Kite 2.4.0

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

_7 November 2026 · [npm](https://www.npmjs.com/package/@box-kite/react) · [Compare v2.3.1...v2.4.0](https://github.com/box-kite/box-kite/compare/v2.3.1...v2.4.0)_

Box Kite now installs nothing but its own engine and asks for React 18 or newer, which is what its components already needed. The rules an AI agent loads are a fifth of the size, and six reported bugs are fixed, among them a system theme that would not switch to dark and a Combobox that screen readers named after its placeholder.

## Box Kite now needs React 18 or newer

The peer range was React 16.14 and newer, but the DataGrid, the date controls, `Popover`, `Dialog`, `Menu` and `Toaster` already needed React 18, and CI only ever tested 18 and 19. The peer range is now `>=18.0.0`, which is what was really supported. No API changed, so this is not a major. But an app on React 16 or 17 that used only `Box` and the simpler components did work before, and npm will now refuse to install this version alongside it.

**On React 16 or 17:** stay on 2.3.x, or upgrade React to 18 or newer first.

## Box Kite installs no third-party packages

`@box-kite/react` now depends on `@box-kite/core` and nothing else. The `css` prop's types used to come from the `csstype` package; they are now a generated copy inside `@box-kite/core`, so `css={{ … }}` is typed exactly as before and nothing extra is installed. The `use-sync-external-store` shim is gone too: the components use React's own `useSyncExternalStore`.

```shell
npm view @box-kite/react dependencies
# { '@box-kite/core': '<this version>' }
```

## The rules your agent loads are a fifth of the size

The rules file, the skill and the Cursor rule that ship in the package were a 90 KB wall loaded on every request that touched a component, and the rules that prevent most mistakes were buried in it. Each rule is now what to write and the one trap, about 19 KB in all, with the same numbers. Nothing was dropped: the reasons, measurements and edge cases behind each rule moved to `references/rules.md` in the skill, under the same number, and `get_rules` in `@box-kite/mcp` reads that long form.

| File                               | Before | After |
| ---------------------------------- | -----: | ----: |
| `.claude/rules/box-kite-rules.md`  |  89 KB | 19 KB |
| `.cursor/rules/box-kite.mdc`       |  91 KB | 20 KB |
| `.claude/skills/box-kite/SKILL.md` |  97 KB | 27 KB |

If you copied the Cursor rule or installed the skill, copy or install it again to pick up the short version.

## Breaking changes

None.

## Fixes

- **`@box-kite/core`'s types pointed at a package it did not install.** Its `css` prop types imported `csstype`, which the package never listed, so a TypeScript app using the engine without React got a missing-module error (or, with `skipLibCheck`, a `css` prop that accepted anything). The types are inside the package now.
- **A `<Box.Theme>` that started on the system theme ignored a `theme` given later.** Passing `theme="dark"` after starting with no `theme` left the page light, because React replayed the provider's own no-op update over the new value. It switches now, and a `design` prop change after a stored design of the same name is no longer lost the same way.
- **A `Combobox` was named after its placeholder, and its list after what was typed.** An `id` written in `props` was replaced by a generated one, so a `<label htmlFor>` of your own named nothing and the browser fell back to the placeholder. And without a `label` prop the list was labelled by the field, which reads out the field's text. The `id` is kept now, and the list carries the field's name: the `label`, your `aria-labelledby`, your `aria-label` or the text of your own `<label>`.
- **Restyling every `Textbox` reached the DataGrid's page number.** `Box.components({ textbox: { styles: { width: 'fit' } } })` stretched the page-number field and wrapped the pagination bar. The field sets its own width now.
- **A `Skeleton` could not be taller than one line.** `<Skeleton height={40} />` drew one thin bar at the top. With one line the bar fills the skeleton now, so a height makes a block.
- **A horizontal `RadioGroup` put its label in the row with the radios.** The label has a line of its own now, and the radios wrap when the row runs out of room.
- **A colour of your own refused the opacity modifier in TypeScript.** `bgColor="ui-primary/35"` rendered correctly but was a type error for a colour added through `Box.extend()` and declared in `Augmented.BoxPropTypes`. Every colour prop that takes `blue-500/40` now takes your colour with a modifier too.
- **Docs.** A custom DataGrid `Cell` is drawn edge to edge with no padding (the built-in text pads itself `px={3}`), which was never written down. The `Grid` example used a `gridCols` prop that does not exist (`gridTemplateColumns`), and `Textbox`, `Textarea`, `Combobox` and `TimeZoneSelect` said an `id` goes in `props`, where the types refuse it: it is the top-level prop.

---

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

