@box-kite/react 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/box-kite-rules.md +23 -1
- package/.claude/skills/box-kite/SKILL.md +134 -427
- package/.claude/skills/box-kite/references/components.md +235 -0
- package/.claude/skills/box-kite/references/extending.md +42 -0
- package/.claude/skills/box-kite/references/patterns.md +27 -0
- package/.claude/skills/box-kite/references/styling.md +222 -0
- package/.cursor/rules/box-kite.mdc +73 -0
- package/AGENTS.md +102 -0
- package/BOX_AI_CONTEXT.md +1124 -153
- package/BOX_KITE_AI_CONTEXT.md +1124 -153
- package/README.md +36 -2
- package/a11y.cjs +1 -1
- package/a11y.d.ts +1 -1
- package/a11y.mjs +3 -2
- package/anchor.cjs +1 -0
- package/anchor.d.ts +14 -0
- package/anchor.mjs +3 -0
- package/behavior.cjs +1 -1
- package/behavior.mjs +118 -136
- package/box.cjs +1 -1
- package/box.mjs +1 -1
- package/client.cjs +1 -1
- package/client.mjs +15 -14
- package/components/accordion.cjs +1 -0
- package/components/accordion.d.ts +114 -0
- package/components/accordion.mjs +251 -0
- package/components/button.cjs +1 -1
- package/components/button.d.ts +3 -0
- package/components/button.mjs +1 -1
- package/components/chart.cjs +1 -1
- package/components/chart.d.ts +2 -1
- package/components/chart.mjs +159 -184
- package/components/checkbox.cjs +1 -1
- package/components/checkbox.d.ts +8 -0
- package/components/checkbox.mjs +1 -1
- package/components/combobox/comboboxChips.d.ts +21 -0
- package/components/combobox/comboboxListbox.d.ts +47 -0
- package/components/combobox.cjs +1 -0
- package/components/combobox.d.ts +121 -0
- package/components/combobox.mjs +563 -0
- package/components/dataGrid/components/dataGridAggregateValue.d.ts +10 -0
- package/components/dataGrid/components/dataGridCell.d.ts +2 -1
- package/components/dataGrid/components/dataGridCellEditor.d.ts +15 -0
- package/components/dataGrid/components/dataGridCellPlaceholder.d.ts +14 -0
- package/components/dataGrid/components/dataGridCellTree.d.ts +16 -0
- package/components/dataGrid/components/dataGridDataSourceError.d.ts +12 -0
- package/components/dataGrid/components/dataGridExport.d.ts +10 -0
- package/components/dataGrid/components/dataGridFooter.d.ts +11 -0
- package/components/dataGrid/contracts/dataGridContract.d.ts +474 -16
- package/components/dataGrid/export/gridExport.d.ts +10 -0
- package/components/dataGrid/gridNavigationContext.d.ts +9 -1
- package/components/dataGrid/models/aggregateCellModel.d.ts +34 -0
- package/components/dataGrid/models/aggregationModel.d.ts +32 -0
- package/components/dataGrid/models/cellKeys.d.ts +11 -0
- package/components/dataGrid/models/cellModel.d.ts +27 -1
- package/components/dataGrid/models/columnModel.d.ts +12 -2
- package/components/dataGrid/models/dataSourceModel.d.ts +177 -0
- package/components/dataGrid/models/editModel.d.ts +77 -0
- package/components/dataGrid/models/exportModel.d.ts +51 -0
- package/components/dataGrid/models/gridModel.d.ts +108 -21
- package/components/dataGrid/models/groupRowCellModel.d.ts +11 -3
- package/components/dataGrid/models/groupRowModel.d.ts +22 -2
- package/components/dataGrid/models/rowModel.d.ts +13 -1
- package/components/dataGrid/models/sourceGroupRowModel.d.ts +45 -0
- package/components/dataGrid/models/sourceLevel.d.ts +71 -0
- package/components/dataGrid/models/sourceRowModel.d.ts +29 -0
- package/components/dataGrid/models/sourceTreeRowModel.d.ts +23 -0
- package/components/dataGrid/models/treeModel.d.ts +94 -0
- package/components/dataGrid/models/treeRow.d.ts +22 -0
- package/components/dataGrid/models/treeRowModel.d.ts +50 -0
- package/components/dataGrid.cjs +1 -1
- package/components/dataGrid.d.ts +4 -2
- package/components/dataGrid.mjs +4484 -2755
- package/components/dialog.cjs +1 -0
- package/components/dialog.d.ts +151 -0
- package/components/dialog.mjs +223 -0
- package/components/dropdown.cjs +1 -1
- package/components/dropdown.d.ts +30 -0
- package/components/dropdown.mjs +256 -244
- package/components/icon.cjs +1 -1
- package/components/icon.d.ts +2 -1
- package/components/icon.mjs +3 -3
- package/components/menu.cjs +1 -0
- package/components/menu.d.ts +234 -0
- package/components/menu.mjs +422 -0
- package/components/overlay.cjs +1 -1
- package/components/overlay.d.ts +30 -26
- package/components/overlay.mjs +45 -75
- package/components/popover.cjs +1 -0
- package/components/popover.d.ts +100 -0
- package/components/popover.mjs +69 -0
- package/components/presence.d.ts +6 -0
- package/components/progress.cjs +1 -0
- package/components/progress.d.ts +47 -0
- package/components/progress.mjs +28 -0
- package/components/radioButton.cjs +1 -1
- package/components/radioButton.d.ts +8 -0
- package/components/radioButton.mjs +1 -1
- package/components/radioGroup.cjs +1 -1
- package/components/radioGroup.d.ts +15 -2
- package/components/radioGroup.mjs +34 -30
- package/components/select.d.ts +9 -0
- package/components/semantics.cjs +1 -1
- package/components/semantics.d.ts +33 -28
- package/components/semantics.mjs +3 -3
- package/components/slider.cjs +1 -0
- package/components/slider.d.ts +98 -0
- package/components/slider.mjs +250 -0
- package/components/svg.cjs +1 -1
- package/components/svg.d.ts +25 -23
- package/components/svg.mjs +3 -3
- package/components/switch.cjs +1 -1
- package/components/switch.d.ts +9 -0
- package/components/switch.mjs +1 -1
- package/components/tabs.cjs +1 -0
- package/components/tabs.d.ts +87 -0
- package/components/tabs.mjs +324 -0
- package/components/textarea.cjs +1 -1
- package/components/textarea.d.ts +14 -0
- package/components/textarea.mjs +1 -1
- package/components/textbox.cjs +1 -1
- package/components/textbox.d.ts +12 -0
- package/components/textbox.mjs +1 -1
- package/components/toaster.cjs +1 -0
- package/components/toaster.d.ts +106 -0
- package/components/toaster.mjs +295 -0
- package/components/tooltip.cjs +1 -1
- package/components/tooltip.d.ts +39 -4
- package/components/tooltip.mjs +56 -54
- package/docs/a11y.md +199 -0
- package/docs/anchor.md +139 -0
- package/docs/components.md +48 -0
- package/docs/index.md +11 -0
- package/docs/props.md +1777 -0
- package/effects.cjs +1 -1
- package/effects.mjs +1 -1
- package/gridExport.cjs +2 -0
- package/gridExport.mjs +244 -0
- package/identity.cjs +1 -0
- package/identity.mjs +12 -0
- package/keyboard.cjs +1 -0
- package/keyboard.mjs +30 -0
- package/layer.cjs +1 -0
- package/layer.mjs +127 -0
- package/motion.cjs +1 -1
- package/motion.mjs +1 -1
- package/package.json +134 -10
- package/placement.cjs +1 -0
- package/placement.mjs +180 -0
- package/platform.cjs +1 -1
- package/platform.mjs +18 -5
- package/react/anchor/useAnchorPosition.d.ts +79 -0
- package/react/dialog/useScrollLock.d.ts +12 -0
- package/react/hooks/usePortalContainer.d.ts +6 -1
- package/react/hooks/useVirtualization.d.ts +24 -35
- package/react/popover/usePopoverLayer.d.ts +87 -0
- package/react-shared.cjs +1 -1
- package/react-shared.mjs +19 -2
- package/rsc.cjs +1 -1
- package/rsc.mjs +1 -1
- package/utils/accordion/accordionUtils.d.ts +48 -0
- package/utils/anchor/anchorUtils.d.ts +79 -0
- package/utils/array/lazyList.d.ts +10 -0
- package/utils/chart/chartPalette.d.ts +15 -0
- package/utils/chart/chartUtils.d.ts +2 -8
- package/utils/combobox/comboboxUtils.d.ts +89 -0
- package/utils/dom/domUtils.d.ts +6 -0
- package/utils/environment/environmentUtils.d.ts +10 -0
- package/utils/export/csvUtils.d.ts +23 -0
- package/utils/export/downloadUtils.d.ts +12 -0
- package/utils/export/xlsxUtils.d.ts +56 -0
- package/utils/export/zipUtils.d.ts +26 -0
- package/utils/keyboard/typeaheadUtils.d.ts +31 -0
- package/utils/menu/menuUtils.d.ts +25 -0
- package/utils/slider/sliderUtils.d.ts +67 -0
- package/utils/tabs/tabsUtils.d.ts +75 -0
- package/utils/toast/toastStore.d.ts +73 -0
- package/utils/virtual/virtualUtils.d.ts +47 -0
- /package/react/{a11y → identity}/useIdentifier.d.ts +0 -0
|
@@ -1,431 +1,138 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: box-kite
|
|
3
|
-
description:
|
|
3
|
+
description: "@box-kite/react (Box Kite) expert — the runtime CSS-in-JS library whose Box component takes 221 typed CSS props and writes the CSS itself. Use for any work with Box props, the Flex/Grid/Button/Textbox/Dropdown/DataGrid components, themes, Box.extend() or Box.components() — and read it before writing props, because the prop names collide with Tailwind's and Chakra's while the numbers mean different things. Also covers installing and updating the package."
|
|
4
|
+
license: MIT
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# @box-kite/react
|
|
7
|
-
|
|
8
|
-
Runtime CSS-in-JS
|
|
9
|
-
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
and
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
`'
|
|
120
|
-
|
|
121
|
-
`
|
|
122
|
-
|
|
123
|
-
`
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
starting rule at `.x` would otherwise lose on specificity to the value it starts from at `.dark .x` or `.x[data-state="open"]` and nothing
|
|
139
|
-
would transition at all; the importance reaches nothing but the before-change style. `Tooltip` and the `Dropdown` popup already carry
|
|
140
|
-
one. For the way back out React unmounts too fast to animate, so either hide instead — `transitionBehavior="allow-discrete"` lets `display`
|
|
141
|
-
transition, flipping it at the _end_, so `<Box display={open ? 'block' : 'none'} opacity={open ? 1 : 0} transitionBehavior="allow-discrete"
|
|
142
|
-
startingStyle={{ opacity: 0 }} />` animates both directions — or hold the node with **`<Presence present>`**
|
|
143
|
-
(`components/presence`): a render prop handed `{ present, state, ref, props }` that keeps rendering its child with `present: false` until the
|
|
144
|
-
child's own CSS says the transition is over, then lets React remove it. `ref` goes on the element carrying the transition (its computed
|
|
145
|
-
`transition-duration`/`animation-duration` _is_ the wait — not a `transitionend` listener, which fires once per property), `props` is
|
|
146
|
-
`{ 'data-state': 'open' | 'closed' }`. Under `prefers-reduced-motion` the measured wait is `0` and the node leaves in the same commit.
|
|
147
|
-
`Tooltip`, the `Dropdown` popup and the DataGrid column menu are built on it. `interpolateSize="allow-keywords"` on a container is what makes
|
|
148
|
-
`height: auto` animate inside it (inherited; Chromium-only, elsewhere it snaps). `Box.configure({ transition: 'colors' | false })` changes
|
|
149
|
-
what the base class transitions, before the first render.
|
|
150
|
-
|
|
151
|
-
**Gradients**: `bgGradient` — a gradient as a _value_, written as a record, so its stops are palette tokens and it is
|
|
152
|
-
themed, takes the opacity modifier and shares one class. The key names the kind and carries its geometry:
|
|
153
|
-
`{ linear: 'r', colors: ['blue-500', 'pink-500'] }` (a direction `t`/`tr`/`r`/`br`/`b`/`bl`/`l`/`tl`, or a number of
|
|
154
|
-
degrees — `{ linear: 135, … }`), `{ radial: 'circle' | 'ellipse' | true, at: 'top left', … }`, `{ conic: 45 | true, … }`.
|
|
155
|
-
Exactly one kind; `at` centres a radial or conic gradient, never a linear one. A stop is any colour value —
|
|
156
|
-
`'blue-500/40'`, `'transparent'`, `'var(--chart-1)'` — or a `[colour, position]` pair (`['sky-500', '20%']`), **two
|
|
157
|
-
minimum**. `interpolate` names the space: `'srgb'`, `'hsl'`, `'oklab'`, `'oklch'`, `'hsl-longer'`, `'oklch-longer'` —
|
|
158
|
-
`'oklch'` is what keeps two stops out of the grey middle sRGB drags them through, and a `-longer` hue turns two stops
|
|
159
|
-
into a spectrum. The record is judged **whole**: one unknown stop, one misspelt key (`interpolat`), two kinds at once,
|
|
160
|
-
and the value emits no rule and no class name. It writes `background-image`, so `bgGradient` and `bgImage` are the
|
|
161
|
-
same property — use one.
|
|
162
|
-
|
|
163
|
-
**Shadows stack, four at a time**: `box-shadow` is one property, so each layer sets its own custom property and all
|
|
164
|
-
four write the same composed declaration — a ring and an elevation coexist instead of the last rule winning.
|
|
165
|
-
`shadow`: `'xxs'`/`'xs'`/`'sm'`/`'md'`/`'lg'`/`'xl'`/`'xxl'` on Tailwind's elevation scale, plus the three original presets
|
|
166
|
-
`'small'`/`'medium'`/`'large'` (which carry their own colour), plus `'none'`. `insetShadow`: `'xxs'`/`'xs'`/`'sm'`, drawn
|
|
167
|
-
inside the border box. `ring`/`insetRing` are a **width in px**, not a scale — `ring={2}` — and unlike `outline` a ring
|
|
168
|
-
joins the stack, follows `borderRadius` and costs no layout. Each layer takes a colour of its own: `shadowColor`,
|
|
169
|
-
`insetShadowColor`, `ringColor` (`currentColor` by default), `insetRingColor` — every colour value, modifier included
|
|
170
|
-
(`shadowColor="blue-500/40"`). A colour prop **shows nothing on its own**, the way `borderColor` does with no border
|
|
171
|
-
width. `'none'` — or `0` on a ring — clears just that layer. `textShadow` (`'xxs'`…`'lg'`, `'none'`) with
|
|
172
|
-
`textShadowColor` is the text-side pair, and `transition="shadow"` covers `box-shadow` and `text-shadow` both.
|
|
173
|
-
|
|
174
|
-
**Filters stack, nine at a time**, for the reason the shadows do: `filter` is one property whose value is a list, so
|
|
175
|
-
each function sets its own custom property and all nine write the same composed declaration. `blur`, `brightness`,
|
|
176
|
-
`contrast`, `grayscale`, `hueRotate`, `invert`, `saturate`, `sepia`, `dropShadow`. **A number is the function's own
|
|
177
|
-
unit**: a percentage for the six that take one (`brightness={110}`, `grayscale={100}`), degrees for `hueRotate`, px for
|
|
178
|
-
`blur` — which also takes Tailwind's scale, `'xs'` (4px) `'sm'` (8px) `'md'` (12px) `'lg'` (16px) `'xl'` (24px)
|
|
179
|
-
`'xxl'` (40px) `'xxxl'` (64px). `dropShadow` is `'xs'`…`'xxl'` with `dropShadowColor`, and it is cast by the **shape** —
|
|
180
|
-
an SVG path, the opaque part of a transparent PNG — where `shadow` draws a rectangle round the box. `'none'` clears
|
|
181
|
-
just that function. The nine `backdrop*` props (`backdropBlur`, `backdropBrightness`, `backdropContrast`,
|
|
182
|
-
`backdropGrayscale`, `backdropHueRotate`, `backdropInvert`, `backdropOpacity`, `backdropSaturate`, `backdropSepia`) are
|
|
183
|
-
the same functions on `backdrop-filter`, which is the glassmorphism half; there is no `backdropDropShadow` and no
|
|
184
|
-
`opacity` filter, because neither means anything on the other side. The older `backdropFilter` prop writes that
|
|
185
|
-
property directly — it and the nine are the same declaration, so use one. `transition="filter"` covers both properties.
|
|
186
|
-
|
|
187
|
-
**Masks and clipping**: `maskImage` takes the same record `bgGradient` does and reads its alpha channel — where the
|
|
188
|
-
gradient is transparent the element is not painted, so `maskImage={{ linear: 'b', colors: ['black', 'transparent'] }}`
|
|
189
|
-
is the whole edge-fade recipe. It also takes `'url(#id)'`, `'var(--name)'` and `'none'`; one mask, not a stack.
|
|
190
|
-
`bgClip` is `'border'`/`'padding'`/`'content'`/`'text'`, and `bgClip="text"` with `color="transparent"` is how a
|
|
191
|
-
gradient becomes lettering — the pairing is deliberately not automatic.
|
|
192
|
-
|
|
193
|
-
**Effects**: `opacity`, `cursor`, `pointerEvents`, `userSelect`, `overflow`
|
|
194
|
-
|
|
195
|
-
**Native controls**: `accentColor` tints the parts of a checkbox, radio, range or progress bar the page does not draw — the palette, its opacity modifier and the system colours, and it inherits, so a form sets it once instead of every control giving up `appearance`. `caretColor` is the text caret. `colorScheme` (`'light'`, `'dark'`, `'light dark'`, `'only light'`, `'only dark'`, `'normal'`) is what native UI follows: scrollbars, form controls, the spellcheck underline — a dark theme that leaves it alone gets light scrollbars over a dark page. `fieldSizing="content"` is a control that grows with what is typed into it, with nothing measuring anything. `scrollbarGutter="stable"` (or `'stable both-edges'`) reserves the scrollbar's space before there is one, so a panel does not shift when its content overflows; `scrollbarWidth`/`scrollbarColor` style the bar itself. `willChange` (`'transform'`, `'opacity'`, `'filter'`, `'scroll-position'`, `'contents'`) promotes an element before it animates rather than during — a hint with a real cost, so it goes on the few elements that move, never on a list
|
|
196
|
-
|
|
197
|
-
## Pseudo-Classes, Breakpoints, State Variants & Container Queries
|
|
198
|
-
|
|
199
|
-
```tsx
|
|
200
|
-
<Box bgColor="blue-500" hover={{ bgColor: 'blue-600' }} disabled={{ opacity: 0.5 }} />
|
|
201
|
-
// Pseudo: hover, focus (:focus-within), focusVisible, hasFocus, active, valid, invalid, optional,
|
|
202
|
-
// disabled, checked, indeterminate, required, selected, hasChecked, hasRequired, hasDisabled,
|
|
203
|
-
// visited (colour props only — the privacy rule), target, open (an [open] element, a popover or a
|
|
204
|
-
// <select>'s picker), placeholderShown, autofill, inRange, outOfRange, inert (the subtree too),
|
|
205
|
-
// rtl / ltr (:dir() — this element's own resolved direction, so ltr matches with no dir set anywhere)
|
|
206
|
-
// Responsive (mobile-first): sm(640) md(768) lg(1024) xl(1280) xxl(1536)
|
|
207
|
-
<Box p={2} md={{ p: 4, hover: { bgColor: 'gray-200' } }} />
|
|
208
|
-
// Device + a11y media keys, same shape as a breakpoint, and they beat every breakpoint in the cascade:
|
|
209
|
-
// pointerCoarse / pointerFine (pointer: coarse|fine — a finger needs a bigger target, and never
|
|
210
|
-
// hovers; ranked below the three preferences), motionReduce (prefers-reduced-motion: reduce),
|
|
211
|
-
// forcedColors (forced-colors: active), contrastMore (prefers-contrast: more).
|
|
212
|
-
// Not nestable in a breakpoint or in each other.
|
|
213
|
-
// The pre-built components come with one dir and no configuration: useRovingFocus reads the element's
|
|
214
|
-
// resolved direction when a sideways arrow arrives, so ArrowLeft is the *next* item in a right-to-left
|
|
215
|
-
// list or grid (APG's rule; Tab/Home/End never flip); a DataGrid column pins to 'START'/'END' of the
|
|
216
|
-
// inline axis; and Overlay measures the direction it was declared in and writes it onto the portalled
|
|
217
|
-
// layer as dir, since a portal inherits nothing. Physical on purpose: Overlay's page coordinates.
|
|
218
|
-
// Reduced motion is already the default — the preference sets --transitionTime to 0s, so every
|
|
219
|
-
// Box stops animating. Declare motionReduce only to replace a movement or keep a safe one.
|
|
220
|
-
<Box transitionDuration={150} motionReduce={{ transition: 'none' }} forcedColors={{ b: 1 }} />
|
|
221
|
-
// Forced colours keep only the system colours, so they are values on every colour prop: Canvas,
|
|
222
|
-
// CanvasText, ButtonFace, ButtonText, Highlight, HighlightText, GrayText, LinkText (keywords, not tokens).
|
|
223
|
-
<Box forcedColors={{ bgColor: 'ButtonFace', color: 'ButtonText' }} />
|
|
224
|
-
// State variants — a selector fragment on the element's own class, so a state your code sets is CSS,
|
|
225
|
-
// not a ternary. Record key = the selector; a key the grammar rejects drops its block, like a bad value.
|
|
226
|
-
// dataAttr {'state=open'} → [data-state="open"], {'loading'} → [data-loading]
|
|
227
|
-
// ariaAttr {'selected'} → [aria-selected="true"] (bare key means ="true"), {'sort=ascending'}
|
|
228
|
-
// has {':checked'} → :has(:checked) · not {hover} / {'data-loading'} → :not(:hover) / :not([data-loading])
|
|
229
|
-
// nth {first|last|only|odd|even|'3'|'2n+1'|'-n+3'|'last 2'} → :first-child … :nth-last-child(2)
|
|
230
|
-
// The attribute itself still goes in `props`. Everything else nests around them, either direction.
|
|
231
|
-
<Box props={{ 'data-state': state }} dataAttr={{ 'state=busy': { bgColor: 'amber-500' } }}
|
|
232
|
-
md={{ hover: { ariaAttr: { selected: { color: 'white' } } } }} />
|
|
233
|
-
<Box bb={1} nth={{ odd: { bgColor: 'slate-500/10' }, 'last 1': { bb: 0 } }} />
|
|
234
|
-
// Somebody else's state: group = an ancestor, peer = a preceding sibling. The key is a state, on the
|
|
235
|
-
// default class (group/peer, Tailwind's names) or on one you name — 'card/hover', 'row/data-state=open'.
|
|
236
|
-
// The ancestor carries the class through `className`; the element's own states stay on the element.
|
|
237
|
-
// hoverGroup/focusGroup/activeGroup/disabledGroup/selectedGroup are the older spelling of the same
|
|
238
|
-
// rule (hoverGroup={{ card }} === group={{ 'card/hover' }}) and share its class. A name that is not a
|
|
239
|
-
// CSS identifier drops the block; a group means nothing in addGlobalStyles.
|
|
240
|
-
<Flex className="card">
|
|
241
|
-
<Box opacity={0} group={{ 'card/hover': { opacity: 1 } }} />
|
|
242
|
-
</Flex>
|
|
243
|
-
<Checkbox className="agree" label={<Box peer={{ 'agree/checked': { color: 'emerald-500' } }}>I agree</Box>} />
|
|
244
|
-
// Pseudo-elements — CSS allows ONE per selector, so it is a slot: a second nested one is a type error,
|
|
245
|
-
// and it lands last on the element's own compound (on the target, not on a group's ancestor).
|
|
246
|
-
// before, after (both come with content: '' — a generated element with none renders nothing),
|
|
247
|
-
// placeholder, selection, marker, firstLine, firstLetter, backdrop, fileButton
|
|
248
|
-
// marker/selection also name descendants, so they go on the <Ul> or the <P>
|
|
249
|
-
// content: 'empty' → '' · 'New' → "New" (text is quoted for you) · 'attr(data-x)'/'counter(s)'/
|
|
250
|
-
// 'url(...)'/'var(--x)'/'"Step " counter(step)' written out as CSS · 'none' turns it off.
|
|
251
|
-
// A value that cannot be quoted or parsed produces no rule and no class name.
|
|
252
|
-
<Box position="relative" before={{ position: 'absolute', inset: 0, bgColor: 'indigo-500' }}
|
|
253
|
-
hover={{ before: { width: 'fit' } }} after={{ content: 'attr(data-count)' }} />
|
|
254
|
-
// On Textbox/Textarea the name means both: a string is the attribute, an object the styles; both at
|
|
255
|
-
// once puts the text in `props`. `placeholderStyles` is the old name for `placeholder` and still works.
|
|
256
|
-
<Textbox props={{ placeholder: 'Search…' }} placeholder={{ color: 'slate-400' }} />
|
|
257
|
-
// Container queries — the same question, asked of the element's own container. `container` makes one
|
|
258
|
-
// (true → container-type: inline-size; a name → container: <name> / inline-size), containerName and
|
|
259
|
-
// containerType ('inline-size' | 'size' | 'normal') are the longhands. `cq` queries the NEAREST one,
|
|
260
|
-
// keyed by size: xs(20rem) sm(24) md(28) lg(32) xl(36) xxl(42), each with a complement maxXs…maxXxl
|
|
261
|
-
// (`not (min-width: …)`, so md and maxMd never both match), and `'name/md'` for a named container.
|
|
262
|
-
// Ranked after every breakpoint and before every preference; one at-rule block per rule, so cq and a
|
|
263
|
-
// breakpoint do not nest in each other. A name that is not a CSS ident drops its block.
|
|
264
|
-
<Flex container="card">
|
|
265
|
-
<Box cq={{ sm: { d: 'row' }, maxSm: { display: 'none' }, 'card/xl': { p: 6 } }} />
|
|
266
|
-
</Flex>
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
## Theme System
|
|
270
|
-
|
|
271
|
-
```tsx
|
|
272
|
-
<Box.Theme> {/* auto-detect system */}
|
|
273
|
-
<Box.Theme theme="dark" use="global"> {/* explicit + applies to <html> */}
|
|
274
|
-
<Box.Theme storageKey="app-theme"> {/* persists to localStorage */}
|
|
275
|
-
// Hook: const [theme, setTheme] = Box.useTheme();
|
|
276
|
-
// setTheme('dark') | setTheme(null) resets to auto
|
|
277
|
-
<Box bgColor="white" theme={{ dark: { bgColor: 'gray-900', hover: { bgColor: 'gray-700' } } }} />
|
|
278
|
-
// App-wide styles on <html> (only with use="global"). Use for inheritable CSS (scrollbarColor, fontFamily, color):
|
|
279
|
-
<Box.Theme use="global" globalStyles={{ scrollbarColor: ['violet-500','transparent'],
|
|
280
|
-
theme: { dark: { scrollbarColor: ['violet-700','gray-900'] } } }} />
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
**Props**: `theme?` (string), `use?` (`'global'`|`'local'`), `storageKey?`, `globalStyles?` (BoxStyleProps — `<html>`, requires `use="global"`). Sets `data-theme` attr + class.
|
|
284
|
-
|
|
285
|
-
## Component System
|
|
286
|
-
|
|
287
|
-
```tsx
|
|
288
|
-
<Box component="card" variant="bordered">
|
|
289
|
-
<Box component="card.header">Title</Box>
|
|
290
|
-
</Box>;
|
|
291
|
-
|
|
292
|
-
export const components = Box.components({
|
|
293
|
-
card: {
|
|
294
|
-
styles: { display: 'flex', d: 'column', p: 4, bgColor: 'white', borderRadius: 8, shadow: 'medium' },
|
|
295
|
-
variants: { bordered: { b: 1, borderColor: 'gray-200', shadow: 'none' } },
|
|
296
|
-
children: { header: { styles: { fontSize: 18, fontWeight: 600 } } },
|
|
297
|
-
},
|
|
298
|
-
subgrid: { extends: 'datagrid', styles: { b: 0, shadow: 'none' } },
|
|
299
|
-
});
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
## Extension System
|
|
303
|
-
|
|
304
|
-
```tsx
|
|
305
|
-
export const { extendedProps, extendedPropTypes } = Box.extend(
|
|
306
|
-
{ 'brand-primary': '#ff6600' }, // CSS variables
|
|
307
|
-
{ imageRendering: [{ values: ['pixelated'] as const, styleName: 'image-rendering' }] }, // New props (`aspectRatio` is built in)
|
|
308
|
-
{
|
|
309
|
-
bgColor: [
|
|
310
|
-
{
|
|
311
|
-
values: ['brand-primary'] as const,
|
|
312
|
-
styleName: 'background-color', // Extend existing
|
|
313
|
-
valueFormat: (v, getVar) => getVar(v),
|
|
314
|
-
},
|
|
315
|
-
],
|
|
316
|
-
},
|
|
317
|
-
);
|
|
318
|
-
// TypeScript: declare module '@box-kite/core/types' { namespace Augmented {
|
|
319
|
-
// interface BoxProps extends ExtractBoxStyles<typeof extendedProps> {}
|
|
320
|
-
// interface BoxPropTypes extends ExtractBoxStyles<typeof extendedPropTypes> {}
|
|
321
|
-
// interface ComponentsTypes extends ExtractComponentsAndVariants<typeof components> {} }}
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
## Dropdown
|
|
325
|
-
|
|
326
|
-
The APG combobox — select-only by default, the editable one with `isSearchable`. **Always pass `label`** (or your own `aria-label` via `props`) — a combobox is
|
|
327
|
-
not named by its content, so without one it has no accessible name. Never add `role="combobox"`/`role="option"`
|
|
328
|
-
by hand; the component supplies the whole pattern.
|
|
329
|
-
|
|
330
|
-
```tsx
|
|
331
|
-
import Dropdown from '@box-kite/react/components/dropdown';
|
|
332
|
-
<Dropdown<string> label="Fruit" defaultValue="a" onChange={(value, values) => {}}>
|
|
333
|
-
<Dropdown.Unselect>Pick...</Dropdown.Unselect>
|
|
334
|
-
<Dropdown.Item value="a">Alpha</Dropdown.Item>
|
|
335
|
-
</Dropdown>;
|
|
336
|
-
// Multiple: <Dropdown label="Fruit" multiple showCheckbox isSearchable searchPlaceholder="Search...">
|
|
337
|
-
// <Dropdown.SelectAll>All</Dropdown.SelectAll> <Dropdown.EmptyItem>No results</Dropdown.EmptyItem>
|
|
338
|
-
// <Dropdown.Display>{(values) => `${values.length} selected`}</Dropdown.Display>
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
**Props**: `label`/`labelProps`, `value`/`defaultValue`, `multiple`, `isSearchable`, `searchPlaceholder`, `hideIcon`, `showCheckbox`, `name`, `onChange`, `itemsProps`, `iconProps`, `variant` (propagates to children). All BoxProps.
|
|
342
|
-
**Sub-components**: `Item<T>` (requires `value`; `disabled` is skipped by the arrows), `Unselect`, `SelectAll`, `EmptyItem`, `Display` (static or `(values, isOpen) => ReactNode`).
|
|
343
|
-
**Keyboard (select-only)**: closed — Down/Up/Enter/Space/Home/End open, a printable character opens at the first match. Open — arrows move, Home/End jump, typing searches, Enter/Space choose, Escape closes unchanged, Tab chooses then leaves. DOM focus stays on the trigger throughout (`aria-activedescendant`).
|
|
344
|
-
**Keyboard (`isSearchable`)**: the `<input>` is the combobox, so printable keys type (no typeahead), Space types a space, Home/End and Left/Right move the caret and drop the highlight, only Down/Up reach the listbox, Enter chooses the highlighted option, and Escape closes before a second Escape clears the field. The field shows the selection as its value, unless a `Dropdown.Display` is drawing it.
|
|
345
|
-
**Style tree**: `dropdown` > `items`, `item` (variants: compact, multiple, highlighted), `unselect`, `selectAll`, `emptyItem`, `icon`.
|
|
346
|
-
|
|
347
|
-
## Select
|
|
348
|
-
|
|
349
|
-
Data-driven dropdown — `data` + `def` instead of children. Shares `dropdown.*` style tree.
|
|
350
|
-
|
|
351
|
-
```tsx
|
|
352
|
-
import Select from '@box-kite/react/components/select';
|
|
353
|
-
<Select<User, number>
|
|
354
|
-
label="User"
|
|
355
|
-
data={users}
|
|
356
|
-
def={{ valueKey: 'id', displayKey: 'name', placeholder: 'Pick...' }}
|
|
357
|
-
value={selected}
|
|
358
|
-
onChange={(value) => setSelected(value!)}
|
|
359
|
-
/>;
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
**SelectDef**: `valueKey` (required), `displayKey`, `display` (`(row) => ReactNode`), `selectedDisplay` (`(rows, isOpen) => ReactNode`), `placeholder`, `selectAllText`, `emptyText`.
|
|
363
|
-
Also: `data`, `label`/`labelProps`, `value`/`defaultValue`, `multiple`, `isSearchable`, `searchPlaceholder`, `showCheckbox`, `hideIcon`, `name`, `onChange`, `itemsProps`, `iconProps`, `variant`, BoxProps. Same combobox pattern as Dropdown — including needing a name, and `isSearchable` switching it to the editable one.
|
|
364
|
-
|
|
365
|
-
## DataGrid
|
|
366
|
-
|
|
367
|
-
```tsx
|
|
368
|
-
import DataGrid from '@box-kite/react/components/dataGrid';
|
|
369
|
-
<DataGrid
|
|
370
|
-
data={users}
|
|
371
|
-
def={{
|
|
372
|
-
rowKey: 'id',
|
|
373
|
-
topBar: true,
|
|
374
|
-
bottomBar: true,
|
|
375
|
-
globalFilter: true,
|
|
376
|
-
rowSelection: { pinned: true },
|
|
377
|
-
showRowNumber: { pinned: true },
|
|
378
|
-
rowHeight: 40,
|
|
379
|
-
visibleRowsCount: 15,
|
|
380
|
-
columns: [
|
|
381
|
-
{ key: 'name', header: 'Name', filterable: true },
|
|
382
|
-
{ key: 'age', header: 'Age', width: 80, align: 'right', filterable: { type: 'number' } },
|
|
383
|
-
{ key: 'status', header: 'Status', filterable: { type: 'multiselect' } },
|
|
384
|
-
{ key: 'country', header: 'Country', pin: 'END' },
|
|
385
|
-
{
|
|
386
|
-
key: 'actions',
|
|
387
|
-
header: '',
|
|
388
|
-
width: 80,
|
|
389
|
-
sortable: false,
|
|
390
|
-
contextMenu: false,
|
|
391
|
-
Cell: ({ cell }) => <Button onClick={() => edit(cell.row.data)}>Edit</Button>,
|
|
392
|
-
},
|
|
393
|
-
],
|
|
394
|
-
rowDetail: { content: (row) => <Details row={row} />, expandOnRowClick: true, expandColumnHeader: 'Details' },
|
|
395
|
-
contextMenu: { sort: true, pin: true, group: false },
|
|
396
|
-
resizerStyle: 'hover',
|
|
397
|
-
}}
|
|
398
|
-
onSelectionChange={(e) => console.log(e.selectedRowKeys)}
|
|
399
|
-
/>;
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
**DataGridProps**: `data`, `def`, `component` (default `'datagrid'`), `loading`, `filters` (predicate[]), `page`/`onPageChange`, `onSortChange`, `onServerStateChange` (`{ page, pageSize, sortColumn, sortDirection, columnFilters, globalFilterValue }`), `onSelectionChange` (`{ action, selectedRowKeys, affectedRowKeys, isAllSelected }`), `expandedRowKeys`/`onExpandedRowKeysChange`, `globalFilterValue`/`onGlobalFilterChange`, `columnFilters`/`onColumnFiltersChange`.
|
|
403
|
-
|
|
404
|
-
**GridDefinition**: `columns` (required), `rowKey`, `rowHeight` (px, default 48), `visibleRowsCount` (number/`'all'`), `showRowNumber` (bool/`{ pinned?, width? }`), `rowSelection` (bool/`{ pinned? }`), `rowDetail` (`{ content, height?, expandOnRowClick?, pinned?, expandColumnWidth?, expandColumnHeader? }`), `pagination` (`{ totalCount, pageSize? }`), `topBar`/`bottomBar`, `title`/`topBarContent`, `globalFilter`, `globalFilterKeys`, `sortable`/`resizable` (default true), `contextMenu` (bool/`{ sort?, pin?, group? }`, default true), `resizerStyle` (`'visible'`/`'hover'`/`'hidden'`), `noDataComponent`.
|
|
405
|
-
|
|
406
|
-
**ColumnType**: `key`, `header`, `width` (px, default 200), `align` (`'start'`/`'end'`/`'center'`, or the physical `'left'`/`'right'`), `pin` (`'START'`/`'END'` — an edge of the **inline axis**, so a pinned column holds under either `dir`; `'LEFT'`/`'RIGHT'` are the older spelling of the same two), `columns` (grouped headers), `Cell` (`({ cell }) => ReactNode`), `sortable`/`resizable` (override grid), `flexible`, `filterable` (`true`=text, `{ type: 'number' }`, `{ type: 'multiselect' }`), `contextMenu` (override grid).
|
|
407
|
-
|
|
408
|
-
**Server-side**: `def={{ pagination: { totalCount }, bottomBar: true }}` + `page={page}` + `onServerStateChange={fetchData}`.
|
|
409
|
-
|
|
410
|
-
**Accessibility (A7, A10)**: the APG grid pattern, over a virtualized body — supplied whole, so add no roles and no `tabIndex`. The scrolling element is `role="grid"` (header and body are `rowgroup`s, rows `role="row"`, cells `role="gridcell"`/`columnheader`, scroll spacers `presentation`). `aria-rowcount`/`-colcount` describe the _whole_ grid and `aria-rowindex`/`-colindex` place each rendered cell in it, header rows first. Also `aria-sort` on sortable headers, `aria-selected` on rows only with `rowSelection`, `aria-busy` while `loading`, `aria-expanded` on group and expanded rows, and a live region announcing the selection count. Keyboard: one cell in the tab order; arrows move it, Home/End along the row, Ctrl+Home/End to the corners (scrolling to unrendered rows), PageUp/Down by a screenful, Enter/Space sorts a header or steps into the cell’s control, F2 always steps in, Escape steps back out. Down/Up keep the _column_ rather than the cell ordinal, so they land under where they started even through a grouped header. The column resizer is APG's window splitter — `role="separator"` with `aria-valuenow`/`-valuemin`/`-valuemax` in pixels; Tab or F2 reaches it, the arrows move it 16px, Home/End go to the narrowest the grid allows and to the grid's own width. Every control the grid draws names itself after what it acts on ("Select row 4", "Column options for Age"); the column menu is the APG menu button. **Give it `def.title`** — a grid is not named by its rows. Known gap: Tab does not yet stay inside the grid.
|
|
411
|
-
|
|
412
|
-
**Style tree**: `datagrid` > `content`, `topBar` > (`globalFilter` > `stats`, `columnGroups` > `icon`|`separator`|`item` > `icon`, `columnVisibility` > `badge`), `filter` > `cell` > `input`, `header` > `cell` > (`contextMenu` > `icon`|`tooltip` > `item` > `icon`|`separator`, `resizer`), `body` > (`cell` > `text`|`rowDetail`, `row`, `groupRow` > `expandButton`, `detailRow`, `empty`), `emptyColumns`, `bottomBar` > (`info`, `clearFilters`, `pagination` > `button`|`info`).
|
|
413
|
-
|
|
414
|
-
## Common Patterns
|
|
415
|
-
|
|
416
|
-
```tsx
|
|
417
|
-
<Flex d="column" gap={4} ai="center" jc="between">{children}</Flex>
|
|
418
|
-
<Flex d="column" gap={2} md={{ d: 'row', gap: 4 }}>{children}</Flex>
|
|
419
|
-
<Grid gridCols={3} gap={4}>{items}</Grid>
|
|
420
|
-
<Box p={4} bgColor="white" borderRadius={8} shadow="medium" />
|
|
421
|
-
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap" />
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
**Group hover**: `<Box group={{ 'parent-class/hover': { opacity: 1 } }}>`
|
|
425
|
-
**SSR**: `import { getStyles, resetStyles } from '@box-kite/react/ssg'` — render, read `getStyles()` into `<style id="box-kite-styles">`, then `resetStyles()`. Needs no DOM.
|
|
426
|
-
**Server Components (React 19)**: works with no `'use client'` and no setup — the `react-server` entry renders Box hook-free and its CSS ships as `<style href precedence>` elements React hoists into `<head>`. Client components in the same app: `Box.configure({ sink: 'element' })` once in a `'use client'` module. Hook-free components (`Flex`, `Grid`, `Button`, `Textbox`, `Textarea`, `RadioButton`, `Icon`, the SVG elements, semantic tags) render on the server too — import them straight into a Server Component; `Dropdown`/`Tooltip`/`Overlay`/`DataGrid`/`Checkbox`/`Switch`/`RadioGroup`/`Select`/`Form` ship a `'use client'` banner, so importing one just opens a client boundary. Client-only: hover-callback children, `Box.Theme` — but `theme={{ dark: {...} }}` styles work server-side (set the theme class on `<html>`). Element-mode rules are in `@layer`, so your own unlayered CSS wins. React 18: keep the default sink.
|
|
427
|
-
**Accessible behaviour** (`@box-kite/react/a11y`, 2.2 KB gz, client-only): `useControllableState` (controlled/uncontrolled with `onChange(value, { reason, event })` — `'escape'`, `'outside-pointer'`, yours), `useDismiss({ enabled, inside: [triggerRef, popupRef], onDismiss })` (Escape reaches the innermost layer only; pass the trigger or it dismisses and reopens in one press), `useFocusReturn({ enabled, returnTo })`, `useRovingFocus({ count, orientation, textOf, onSelect })` → `{ activeIndex, onKeyDown, itemProps(i), activeItem() }` (arrows, Home/End, typeahead, disabled skipped; `focusItems: false` for `aria-activedescendant`), `useIdentifier('select')` (React's `useId`, usable as a selector). They supply mechanics, never roles or ARIA. `VisuallyHidden` (`@box-kite/react/components/visuallyHidden`) is a Box that clips itself away instead of hiding, so it stays in the accessibility tree — and it renders on a server.
|
|
428
|
-
**Config**: `Box.configure({ sink: 'cssom' | 'textContent' | 'string' | 'element', classNames: 'hashed' | 'readable' | 'stable' })` — before the first render.
|
|
429
|
-
**Portals**: `Overlay` (`components/overlay`) renders children into `#box-kite-portal` at the place it is declared — escapes `overflow: hidden` and clipped ancestors, no ARIA and no open state.
|
|
430
|
-
**Form controls** (`components/checkbox`, `components/switch`, `components/radioGroup`, `components/radioButton`): real native inputs, so focus, the checked state and form submission come from the platform. `label` is the prop that matters — it renders the text inside a `<label>` wrapping the input, so there is no `htmlFor`/`id` pair and the whole row is clickable; `labelProps` styles that label, every other Box prop styles the control. `<Checkbox label="Accept the terms" name="terms" />`, `<Checkbox label="Select all" indeterminate />` (sets the DOM property **and** `aria-checked="mixed"`), `<Switch label="Email notifications" name="notify" />` (`role="switch"`; Space and Enter both toggle, Enter does not submit the form). `<RadioGroup label="Plan" name="plan" defaultValue="free" onChange={(v, { reason }) => …}>` with `<RadioGroup.Item value="free" label="Free" />` children: `role="radiogroup"` named by the label, one shared `name` (generated if omitted), `orientation` `vertical`|`horizontal`, arrows move **and** select with wrapping and disabled items skipped, and Tab enters/leaves the set once — the platform's own tab order is left alone. `RadioButton` alone is a plain radio and still renders on a server. `checked`/`disabled`/`indeterminate` are each a state _and_ a pseudo-class style prop: pass a boolean, or `[state, styles]` for both.
|
|
431
|
-
**Tooltip** (`components/tooltip`): the APG pattern on that layer. `<Tooltip content="Deletes the row">{(trigger) => <Button {...trigger}>Delete</Button>}</Tooltip>` — the child is a render prop because `aria-describedby` has to land on the control itself; the bag is `{ ref, props }`, so one spread on a Box component, or `<button ref={trigger.ref} {...trigger.props}>` on a plain one. The `ref` is what positions the bubble under the trigger. Gives you `role="tooltip"`, the describedby wiring while open, hover **and** focus opening (`openDelay` 300 ms, focus ignores it), Escape with focus left in place, and WCAG 1.4.13: the pointer can travel onto the bubble (`closeDelay` 150 ms) and nothing hides it on a timer. `open`/`defaultOpen`/`onOpenChange(open, { reason })` with reasons `hover`|`focus`|`pointer-leave`|`blur`|`escape`; other Box props style the bubble.
|
|
7
|
+
# @box-kite/react
|
|
8
|
+
|
|
9
|
+
Runtime CSS-in-JS. `Box` takes 221 typed CSS props and generates the CSS for them at runtime — no stylesheet, no class name to invent, and two elements written with the same value share one class.
|
|
10
|
+
|
|
11
|
+
## This library is not in your training data
|
|
12
|
+
|
|
13
|
+
It was renamed at 1.0.0 (September 2026) and its prop surface nearly doubled on the way, so a plausible-looking memory of it is a memory of a different library. Four facts first, because each one is silently wrong in the APIs this most resembles:
|
|
14
|
+
|
|
15
|
+
- **The dividers are per prop.** Spacing is ÷4 (`p={4}` is 1rem), `fontSize` is ÷16 (`fontSize={14}` is 14px), `borderRadius` is ÷4, and border width and `lineHeight` are direct px (`b={1}` is 1px).
|
|
16
|
+
- **There is no `style` attribute.** Every visual value is a prop; the one-off nothing has a prop for is `css={{ … }}`, which still compiles to a shared class.
|
|
17
|
+
- **The element comes from the component, not from `tag`.** `<Flex>`, `<Button>`, `<H1>`, `<Ul>`, `<Path>` — `<Box tag="…">` is the fallback, not the idiom.
|
|
18
|
+
- **HTML attributes go in `props`.** `<Link props={{ href: '/about' }}>`; an `href` written at the top level typechecks and is then dropped.
|
|
19
|
+
|
|
20
|
+
## The numbers, measured
|
|
21
|
+
|
|
22
|
+
The divider is per prop, so the same number is a different length depending on which prop it is written on — `p={4}` is 16px and `b={4}` is 4px. Every row here is what the engine emitted for that example, not what a doc claims about it:
|
|
23
|
+
|
|
24
|
+
| Prop | Which props | Written | Emitted |
|
|
25
|
+
| ------------------- | ---------------------------------------------------------- | -------------------------- | ---------------------------- |
|
|
26
|
+
| `p` | spacing — `p`/`m`/`gap`/`px`/`py`/`pt`… all of them | `p={4}` | `padding: 1rem` |
|
|
27
|
+
| `width` | sizing — `width`/`height` and the four `min`/`max` props | `width={4}` | `width: 1rem` |
|
|
28
|
+
| `borderRadius` | the corner radius, on the spacing scale | `borderRadius={4}` | `border-radius: 1rem` |
|
|
29
|
+
| `b` | border width — `b`/`bx`/`by`/`bt`/`bs`… | `b={4}` | `border-width: 4px` |
|
|
30
|
+
| `fontSize` | the one that is ÷16 | `fontSize={14}` | `font-size: 0.875rem` |
|
|
31
|
+
| `lineHeight` | and `letterSpacing` | `lineHeight={24}` | `line-height: 24px` |
|
|
32
|
+
| `strokeWidth` | SVG lengths — and the geometry props `cx`/`cy`/`r`/`x`/`y` | `strokeWidth={4}` | `stroke-width: 4` |
|
|
33
|
+
| `animationDuration` | every animation and transition time | `animationDuration={1100}` | `animation-duration: 1100ms` |
|
|
34
|
+
|
|
35
|
+
## Read this next
|
|
36
|
+
|
|
37
|
+
| Read | When the question is |
|
|
38
|
+
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
39
|
+
| [`references/styling.md`](references/styling.md) — Styling reference | writing props: the categories, the six kinds of nesting, the theme |
|
|
40
|
+
| [`references/components.md`](references/components.md) — Components reference | choosing a component, or wiring a Dropdown, a Select or a DataGrid |
|
|
41
|
+
| [`references/extending.md`](references/extending.md) — Extending Box | a value used twice, or a prop that does not exist yet |
|
|
42
|
+
| [`references/patterns.md`](references/patterns.md) — Patterns and the rest of the surface | server rendering, the behaviour hooks, portals, form controls, the tooltip |
|
|
43
|
+
| `node_modules/@box-kite/react/docs/props.md` | one prop exactly: all 221, the CSS each writes, one measured example |
|
|
44
|
+
| `node_modules/@box-kite/react/BOX_KITE_AI_CONTEXT.md` | the long-form reference, when a reference above is not enough |
|
|
45
|
+
|
|
46
|
+
With the package not installed, the prop reference is https://www.box-kite.dev/props.md, and https://www.box-kite.dev/llms.txt indexes every documentation page as markdown beside it.
|
|
47
|
+
|
|
48
|
+
## Installing and updating
|
|
49
|
+
|
|
50
|
+
Detect the package manager from the lock file rather than asking:
|
|
51
|
+
|
|
52
|
+
| Lock file | Manager | Install | Update |
|
|
53
|
+
| ------------------- | ------- | ----------------------------- | ------------------------------ |
|
|
54
|
+
| `package-lock.json` | npm | `npm install @box-kite/react` | `npm update @box-kite/react` |
|
|
55
|
+
| `yarn.lock` | yarn | `yarn add @box-kite/react` | `yarn upgrade @box-kite/react` |
|
|
56
|
+
| `pnpm-lock.yaml` | pnpm | `pnpm add @box-kite/react` | `pnpm update @box-kite/react` |
|
|
57
|
+
| `bun.lockb` | bun | `bun add @box-kite/react` | `bun update @box-kite/react` |
|
|
58
|
+
|
|
59
|
+
`@box-kite/react` needs React 18 or 19 as a peer, and installs `@box-kite/core` (the engine) with itself.
|
|
60
|
+
|
|
61
|
+
## Still working, but no longer the spelling to write
|
|
62
|
+
|
|
63
|
+
- `placeholderStyles` — The name is `placeholder` now — this spelling still works and means the same thing.
|
|
64
|
+
- `hoverGroup` — `group={{ 'card/hover': … }}` — the same rule, and every other state beside it.
|
|
65
|
+
- `focusGroup` — `group={{ 'card/focus': … }}`.
|
|
66
|
+
- `activeGroup` — `group={{ 'card/active': … }}`.
|
|
67
|
+
- `disabledGroup` — `group={{ 'card/disabled': … }}`.
|
|
68
|
+
- `selectedGroup` — `group={{ 'card/selected': … }}`.
|
|
69
|
+
- `@box-kite/react/components/baseSvg` — Since SV3 every SVG element has a component, and `Svg` is this one without the presets: `<Svg viewBox="0 0 24 24" width="1.5rem">`.
|
|
70
|
+
- `@box-kite/react/components/dataGrid#onSelectionChange` — `onSelectedRowKeysChange` is the selection itself, with the `action` as a named reason — the spelling every component reports a change under.
|
|
71
|
+
- `@box-kite/react/components/dataGrid#onPageChange` — `onPaginationChange` reports both pager values with a reason, which is what says whether the page changed because the user navigated or because a filter reset it.
|
|
72
|
+
- `@box-kite/react/components/dataGrid#onPageSizeChange` — `onPaginationChange` covers it: a new page size always returns to page 1, so the two were never separate changes.
|
|
73
|
+
- `@box-kite/react/components/dataGrid#onSortChange` — `onSortingChange` reports the sort as one value with a reason beside it.
|
|
74
|
+
- `@box-kite/react/components/dropdown#onChange` — `onValueChange` is what every component reports a value change under, and it carries the reason — which is the thing a `(value, values)` pair cannot say.
|
|
75
|
+
- `@box-kite/react/components/radioGroup#onChange` — `onValueChange` is what every other component reports a value change under, and a group is not an `<input>` — this spelling still works and means the same thing.
|
|
76
|
+
- `@box-kite/react/components/select#onChange` — `onValueChange` is what every component reports a value change under, and it carries the reason.
|
|
77
|
+
- `@box-kite/react/components/semantics` — `MenuList` is this element now.
|
|
78
|
+
|
|
79
|
+
## The rules
|
|
80
|
+
|
|
81
|
+
1. **NEVER use `style={{ }}`** — always use Box props. Missing prop? Create with `Box.extend()`
|
|
82
|
+
2. **NEVER `<Box tag="...">`** for common elements — use `<Button>`, `<Link>`, `<H1>`, `<P>`, `<Ul>`/`<Li>`, `<Nav>`, `<Flex>`, `<Grid>`, and for SVG `<Svg>`, `<Path>`, `<Circle>`, `<Rect>`, `<SvgText>`… from `components/svg`
|
|
83
|
+
3. **An icon from lucide/Tabler/react-icons goes in `<Icon>`** (`components/icon`) — `<Icon size={5} color="amber-500"><Sun /></Icon>`. `size` is the ÷4 scale (the set's own `size` prop is pixels), and no `label` means `aria-hidden`. SVG you draw yourself is `<Svg>`, not `<Icon>`. An icon from any other set comes through the same `<Icon>`: `unplugin-icons` (`~icons/<set>/<name>`) at build time, `@iconify/react` when the name is data
|
|
84
|
+
4. **NEVER `<Box display="flex/grid">`** — use `<Flex>` / `<Grid>` components
|
|
85
|
+
5. **fontSize divider is 16** (not 4): `fontSize={14}` → 14px
|
|
86
|
+
6. **Spacing divider is 4**: `p={4}` → 16px (1rem)
|
|
87
|
+
7. **Border width and lineHeight are direct px**: `b={1}` → 1px. **borderRadius uses divider 4**: `borderRadius={2}` → 8px
|
|
88
|
+
8. **A gradient fill is a value, not an attribute**: `fill="url(#sky)"`, `stroke="var(--chart-1)"`, `clipPath="url(#frame)"` — so it can be themed and hovered. **A dashboard shape is `components/chart`**: `<Sparkline data={[…]}/>`, `<ProgressRing value={0.6}/>`, `<Gauge>`, `<MiniDonut>` — Box props, no chart library
|
|
89
|
+
9. **A CSS variable is a prop too**: `vars={{ 'color-revenue': 'sky-500' }}` declares `--color-revenue` on the element and everything inside it — the answer for markup this library does not render. **A third-party chart goes in `<ChartContainer series={['revenue', 'cost']}>`** (`components/chart`), which declares `--chart-1..6` in both themes and one `--color-<series>` per series, so `<Line stroke="var(--color-revenue)"/>` is all the chart ever says about colour
|
|
90
|
+
10. **SVG lengths have no divider and no unit**: `strokeWidth={2}` → `stroke-width: 2` (user units), same for `strokeDasharray`/`strokeDashoffset` and the geometry props `cx`/`cy`/`r`/`rx`/`ry`/`x`/`y`. `<Rect width={40} height={40}>` and `<Path d="M…">` take those as the SVG attributes they are — the Box props of those names mean something else
|
|
91
|
+
11. **HTML attributes go in `props` prop**: `<Link props={{ href: '/about' }}>` not `<Link href>`
|
|
92
|
+
12. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
|
|
93
|
+
13. **An animation is a prop**: `animation="spin|pulse|bounce|ping"` (keyframes included, and they stop under reduced motion on their own), `Box.keyframes({ name: { from, to } })` for your own — its steps are Box props. **Times are milliseconds**: `animationDuration={1100}`, `transitionDelay={150}`. `transition` takes a group (`colors`, `transform`, `opacity`, `shadow`, `size`, `filter`), and `translateX`/`translateY`/`rotate`/`scale` are longhands that compose. **A spring is two props**: `transitionTimingFunction="spring-bouncy"` with `transitionDuration="spring-bouncy"` (also `spring`, `spring-gentle`, `spring-snappy`; `Box.spring({ stiffness, damping })` for your own)
|
|
94
|
+
14. **An entrance is a prop too**: `startingStyle={{ opacity: 0, translateY: 2 }}` is what a just-mounted element starts from (`@starting-style`, plain props only — nest a breakpoint or a theme _around_ it). Going out, React unmounts too fast to animate: either hide instead with `display` + `transitionBehavior="allow-discrete"`, which flips `display` at the _end_, or hold the node with **`<Presence present>`** (`components/presence`) — a render prop given `{ present, ref, props }`, where `ref` goes on the element carrying the transition. `interpolateSize="allow-keywords"` on a container is what makes `height: auto` animate (Chromium-only, snaps elsewhere)
|
|
95
|
+
15. **`data-*` and `aria-*` are attributes, so they go in `props`** — `props={{ 'data-state': 'open' }}`. Written at the top level (`<Box data-state="open">`) they typecheck and are silently dropped
|
|
96
|
+
16. **A state your own code sets is a nested prop too**: `dataAttr={{ 'state=open': { … } }}` → `[data-state="open"]` (a bare key is presence: `[data-loading]`), `ariaAttr={{ selected: { … } }}` → `[aria-selected="true"]`, `has={{ ':checked': { … } }}`, `not={{ hover: { … } }}` — keyed by state name, so it stays typed — and `nth={{ odd: { … }, 'last 1': { … } }}` → `:nth-child(odd)`/`:nth-last-child(1)`, which also takes `first`/`last`/`only`/`even` and an `An+B` formula. The attribute itself still goes in `props`; everything else (a breakpoint, a theme, a group, `startingStyle`) nests around them in either direction. A key the grammar rejects drops its whole block, the way an unmatched value does
|
|
97
|
+
17. **A pseudo-element is a nested prop, and CSS allows one**: `before`/`after` (which come with `content: ''`, because a generated element with none renders nothing), `placeholder`, `selection`, `marker`, `firstLine`, `firstLetter`, `backdrop`, `fileButton`. It is a slot, not a list — a second nested one is a type error — and it lands last on the element's own selector, so it composes with everything else in either direction. **`content` takes text and quotes it for you** (`content="New"` → `content: "New"`); `attr()`/`counter()`/`url()`/`var()` and a quoted sequence (`'"Step " counter(step)'`) are written out as CSS. `marker` and `selection` reach descendants, so they go on the `<Ul>` or the `<P>`. On a `Textbox`/`Textarea` a `placeholder` **string** is the attribute and an object is the styles; for both, the text goes in `props`
|
|
98
|
+
18. **A forced-colors mode keeps only the system colours**, so they are values on every colour prop: `forcedColors={{ bgColor: 'ButtonFace', color: 'ButtonText' }}` (also `Canvas`, `CanvasText`, `Highlight`, `HighlightText`, `GrayText`, `LinkText`). They are keywords, not tokens — a state signalled by fill alone needs them or it reads identically on and off
|
|
99
|
+
|
|
100
|
+
19. **A component that answers to its own space uses `cq`, not a breakpoint**: `container` makes an element a query container (`true`, or a name — both mean `container-type: inline-size`; `containerName`/`containerType` are the longhands), and `cq={{ md: { d: 'row' } }}` → `@container (min-width: 28rem)`. Six sizes, `xs` 20rem through `xxl` 42rem — **the container scale, not the breakpoint one** — each with a complement (`maxMd` is `not (min-width: 28rem)`), and `cq={{ 'sidebar/md': { … } }}` addresses a container by name. Ranked after every breakpoint and before every preference; one at-rule block per rule, so `cq` and a breakpoint do not nest inside each other
|
|
101
|
+
|
|
102
|
+
20. **An opacity is part of the colour, not a second prop**: `bgColor="blue-500/40"`, `borderColor="black/10"`, `color="currentColor/60"` — a slash and a percentage on any colour value, on every colour prop and on a `vars` entry. It compiles to `color-mix(in oklab, var(--blue-500) 40%, transparent)`, so the token stays a variable (still themed, still one shared class) — unlike `opacity`, which fades the element, its text and its children. The palette itself is **Tailwind 4.3's, in OKLCH**: twenty-six families (the five neutrals, `mauve`/`mist`/`olive`/`taupe`, and seventeen hues) of eleven steps, `50`–`950`. A token the palette does not have, or a percentage outside 0–100, produces no rule and no class name; a variable from `Box.extend()` takes the modifier too
|
|
103
|
+
|
|
104
|
+
21. **A gradient is a value, and a shadow is a stack.** `bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }}` — the key names the kind (`linear` a direction `t`/`tr`/`r`/… or an angle in degrees, `radial` a shape, `conic` a start angle), `colors` are the stops in order, and a stop is any colour value: a token, `blue-500/40`, `var(--chart-1)`, or a `[colour, position]` pair. `at` centres a radial or conic one, `interpolate="oklch"` keeps two stops out of the grey middle sRGB drags them through, and `oklch-longer` turns two stops into a spectrum. Two stops minimum, and the record is judged **whole** — one bad stop or one unknown key and the value emits nothing. It writes `background-image`, so it and `bgImage` are the same property. **Four shadows stack** rather than overwriting one `box-shadow`: `shadow` (`xxs`…`xxl` on Tailwind's scale, or the presets `small`/`medium`/`large`), `insetShadow` (`xxs`/`xs`/`sm`), and `ring`/`insetRing`, which are a **width in px** (`ring={2}`) that follows `borderRadius` and costs no layout, unlike `outline`. Each takes a colour of its own (`shadowColor`, `ringColor`, …) which shows nothing until the layer is painted; `none` — or `0` on a ring — clears just that layer. `textShadow` is `xxs`…`lg` with `textShadowColor`, and `transition="shadow"` covers both properties
|
|
105
|
+
|
|
106
|
+
22. **Nine filters stack too, and a mask is a gradient.** `blur`, `brightness`, `contrast`, `grayscale`, `hueRotate`, `invert`, `saturate`, `sepia` and `dropShadow` each set their own layer of one composed `filter`, so a blur and a desaturation coexist. **A number is the function's own unit** — `brightness={110}` is a percentage, `hueRotate={90}` degrees, `blur={3}` pixels — and `blur` takes Tailwind's scale as well (`xs` 4px … `xxxl` 64px). `dropShadow` (`xs`…`xxl`, with `dropShadowColor`) is cast by the **shape**, where `shadow` draws a rectangle; `none` clears one function. The nine `backdrop*` props are the same functions behind the element — `backdropBlur="sm" backdropSaturate={180}` over something translucent is glassmorphism — and they and the older `backdropFilter` are the same property, so use one. **`maskImage` takes the `bgGradient` record** and reads its alpha: `maskImage={{ linear: 'b', colors: ['black', 'transparent'] }}` fades an edge, `url(#id)` masks by a shape. **`bgClip="text"` needs `color="transparent"`** beside it, and together they are how a gradient becomes type
|
|
107
|
+
|
|
108
|
+
23. **An alignment can be overflow-safe, and a ratio is a value.** The box-alignment set is complete: `jc`/`ai` plus `justifyItems`/`placeItems`/`placeContent`/`alignContent`/`alignSelf`/`justifySelf`, and every one of them takes `'safe center'`, `'safe start'`, `'safe end'` and the `'unsafe …'` twins — `safe center` centres until the content stops fitting and then aligns to `start`, where plain `center` overflows both edges and the half above the scrollable origin cannot be reached. `aspectRatio` is `'square'` (1/1), `'video'` (16/9), a compact ratio (`'4/3'`) or a number; `'4:3'` emits nothing. `insetX`/`insetY` are `inset-inline`/`inset-block`, the way `mx`/`my` are the margin shorthands, and every inset prop now takes `'auto'` and the fractions (`inset="1/4"`, `top="-1/3"`). **The parts of a native control the page does not draw are props too**: `accentColor` (a checkbox tick, a radio dot, a range track — it inherits, so a form sets it once), `caretColor`, `colorScheme` (`'light dark'` — what scrollbars and form controls follow), `fieldSizing="content"` (a field that grows as it is typed into), `scrollbarGutter="stable"` (the scrollbar's space reserved before there is one) and `willChange`, which costs memory: put it on the few elements that move, never on a list.
|
|
109
|
+
|
|
110
|
+
24. **Somebody else's state is `group` or `peer`.** `group={{ 'card/hover': { opacity: 1 } }}` → `.card:hover .className` (an ancestor), `peer={{ 'agree/checked': { color: 'emerald-500' } }}` → `.agree:checked ~ .className` (a preceding sibling); the ancestor or sibling carries the class through `className`, and a bare key uses Tailwind's default names (`group`, `peer`). The state half is the vocabulary `not` takes — any pseudo-class, or a `data-`/`aria-` prefix for an attribute the ancestor carries (`'row/data-state=open'`). **The element's own states stay on the element**: `group={{ 'card/hover': { hover: … } }}` is `.card:hover .x:hover`. `hoverGroup`/`focusGroup`/`activeGroup`/`disabledGroup`/`selectedGroup` are the older spelling of the same rule and share its class. **Eight more browser states are pseudo-class props**: `open` (an `[open]` element, a popover, a `<select>`'s picker), `placeholderShown`, `autofill`, `inRange`/`outOfRange`, `target`, `inert` (the whole inert subtree) and `visited`, which takes **colour properties only** — the browser refuses the rest so a page cannot read a reader's history. **`pointerCoarse`/`pointerFine`** are two more media keys beside the preferences, ranked below them: a finger needs a bigger target and never hovers.
|
|
111
|
+
|
|
112
|
+
25. **A side is logical, and the direction is a state.** `ps`/`pe`, `ms`/`me`, `bs`/`be`, `insetStart`/`insetEnd` and `borderRadiusStart`/`borderRadiusEnd` (plus the four logical corners, **block axis first**: `borderRadiusStartStart`…) are the two sides of the inline axis the `px`/`mx`/`insetX` pairs already work 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 no re-render. Same dividers as the physical twins (÷4, and direct px for `bs`/`be`); `textAlign` takes `'start'`/`'end'` beside `'left'`/`'right'`. For what a logical property cannot express — an arrow that must point the other way — `rtl={{ flip: 'xAxis' }}` and `ltr={{ … }}` are ordinary pseudo-class keys compiling to `:dir(rtl)`/`:dir(ltr)`, **not** Tailwind's `[dir="rtl"] &`: the direction belongs to _this_ element, so a `<bdi>` or a `dir="auto"` is seen. With no `dir` anywhere the document is left-to-right, so `ltr` matches — it is a state, not an attribute you have to write, and the `dir` attribute itself goes in `props`. **The pre-built components come with it**: the arrow keys follow the reading order (`ArrowLeft` is the _next_ item in a right-to-left list, APG's rule), a DataGrid column pins to `'START'`/`'END'` of the inline axis (`'LEFT'`/`'RIGHT'` still mean those two) and `align` takes `'start'`/`'end'`, and a popup keeps the direction it was declared in, by staying in that subtree (rule 30) or, on the portal fallback, by carrying it across. `Overlay`'s page coordinates stay physical on purpose — a measured pixel has no reading order.
|
|
113
|
+
|
|
114
|
+
26. **The 5% is `css`, and it is still a class.** `css={{ mixBlendMode: 'multiply', WebkitLineClamp: 2 }}` takes a style object for the properties this library has no prop for and compiles it through the same pipeline as every other prop — one shared class, nested anywhere a prop nests (`hover={{ css: … }}`, `md`, `theme`, `dataAttr`, `before`, `startingStyle`, a keyframes step), rendered on a server — so it is never `style={{ }}`. Names are camelCase and typed by csstype (a misspelt property is a compile error, `WebkitLineClamp` → `-webkit-line-clamp`); values are CSS written as they stand, a number included, so a length wants its unit (`width: '100px'`, the type refuses `100`) and a colour token resolves the way a `vars` value does (`outlineColor: 'sky-500'`). It is sorted **last**, so on one element it wins the property a typed prop also names. A value carrying `;` or a brace is dropped, that entry only. Reach for a typed prop first, `Box.extend()` for anything used twice, `css` for the one-off.
|
|
115
|
+
|
|
116
|
+
27. **A floating layer is anchored in CSS, not measured in JS.** `anchorName="trigger"` names the anchor (the `--` is optional and added for you), and on the layer `position="absolute"` + `positionAnchor="trigger"` + `positionArea="block-end span-all"` place it — `positionArea` is which cell of the 3×3 grid around the anchor to sit in, **block axis first, inline axis second**, two keywords from one family (`'block-end center'`, `'top left'`; `'top inline-start'` mixes families and emits nothing). Offset it with an ordinary margin. `positionTryFallbacks="flip-block"` is the flip (`flip-inline`, `flip-start`, space-combined into one candidate, comma-separated into a list tried in order), `positionTryOrder` takes the roomiest instead of the first that fits, and `positionVisibility="anchors-visible"` hides the layer once its anchor scrolls away. `justifySelf`/`alignSelf` take `'anchor-center'`. **Two traps, both measured**: a candidate has to fit on _both_ axes, so `positionArea="block-end center"` with a layer wider than its anchor disqualifies every position and `flip-block` does nothing — span the axis you keep (`'block-end span-all'`); and `positionVisibility` hides at paint time, so computed `visibility` still reads `visible`. Chrome 125+/Firefox 147+/Safari 26+; elsewhere the layer renders unpositioned, so the portable path is `useAnchorPosition` (rule 28), which measures instead.
|
|
117
|
+
|
|
118
|
+
28. **A length can come off the anchor, and `useAnchorPosition` writes the whole placement for you.** Every sizing prop takes an `anchor-size()` value and every single-side inset prop an `anchor()` one: `minWidth="anchor-size(width)"` is a popup as wide as its trigger with nothing measured, `maxHeight="anchor-size(height, 20rem)"` falls back when there is no anchor, and `top="anchor(bottom)"`/`insetStart="anchor(left)"` place a layer by hand where the 3×3 grid has no cell for it. The name inside is optional (it defaults to `positionAnchor`'s) and takes the same optional `--`; the axis names are `width`/`height`/`block`/`inline` and their `self-` twins, the edges `top`/`right`/`bottom`/`left`/`start`/`end`/`center`/`inside`/`outside` or a percentage. **The hook is `useAnchorPosition` from `@box-kite/react/anchor`**: `const { css, anchorProps, layerProps } = useAnchorPosition({ side: 'bottom', align: 'start', offset: 2, matchWidth: true })`, spread on trigger and layer. `side` is `top`/`bottom` (block axis) or `start`/`end` (inline, so it mirrors in a right-to-left page), `align` lines the layer up with one of the anchor's edges, `offset` is the ÷4 scale, and `flip` is on by default — offering the side's own axis, the alignment's and both, because a candidate has to fit on _both_ axes and a lone `flip-block` does nothing for a layer that overflows the cross one. **The pre-built layers take the same four**: `Overlay` and `Tooltip` place with `side`/`align`/`offset`/`flip` (plus `anchor`, the element to hang off, and `matchWidth`), and the `Dropdown` popup and the DataGrid column menu are those. A gap is `offset`, not a margin or a translate. Where the browser has no anchor positioning it measures instead — same model, flip and shift — and `css` says which path ran. **Three things to know**: the layer is `position: fixed`, so it escapes every `overflow: hidden` ancestor without a portal, but _not_ a transformed one and not the page's stacking order (`Overlay` is this hook plus the top layer, and is still the answer when the layer must come out on top of everything); a flip is **sticky** — the browser keeps it until the layer is laid out afresh, so a popup that mounts when it opens always picks the side that fits while one that stays mounted keeps the side it first chose, and in the **top layer** it is never re-evaluated on scroll at all (rule 30); and the anchor's **name is an inline style rather than a prop**, because an identity is per instance and a class for it would be a rule per instance that is never freed. Which side the browser chose is `trackSide`, and it arrives too late for an entrance: reading the used `position-area` _is_ the style resolution `@starting-style` computes from, so only an exit can depend on it.
|
|
119
|
+
|
|
120
|
+
29. **A panel with content in it is `<Popover>`, and it needs no portal.** `<Popover label="Filters" trigger={(t) => <Button {...t}>Filters</Button>}><Checkbox label="Only mine" /></Popover>` (`components/popover`) is the platform's Popover API: the panel is in the browser's **top layer**, so it paints above every stacking context and outside every clipped ancestor, and **light dismiss (Escape, an outside press) and focus return are the browser's** rather than something to wire up. Note the shape — `trigger` is the render prop and the **children are the panel**, the other way round from `Tooltip`, whose `content` is the description and whose children are the trigger. It takes `useAnchorPosition`'s four placement props (`side`/`align`/`offset`/`flip`, plus `matchWidth`), and it supplies what the attribute does not: `role="dialog"` named by `label`/`labelledBy`, `aria-expanded`/`aria-haspopup`/`aria-controls` on the trigger, focus into the panel on open (`autoFocus`), and `onOpenChange(open, { reason })` with `trigger`/`escape`/`outside-pointer`/`imperative`. **Four things to know, all measured.** The **trigger has to be a button**, because the toggle is the browser's own `popovertarget` — and never add an `onClick` toggle of your own: light dismiss closes on `pointerdown`, so the `click` after it reads "closed" and reopens the panel, and pressing the trigger would never close it. The **panel is always rendered**, closed being `display: none` rather than unmounted, so the exit is a CSS transition (`transitionBehavior="allow-discrete"`, already in its component styles) and **never `<Presence>`** — gate expensive children yourself with `{open ? … : null}`. A **close cannot be refused**: `beforetoggle` is cancelable opening and not closing, so a controlled `<Popover open>` hears about a dismissal only afterwards and keeping `open` true re-shows the panel. And because nothing is portalled, the panel keeps the theme, the custom properties, the direction and the tab order it was declared in — which `Overlay` gets the same way since rule 30. Use `Overlay` only for positioning with no pattern on it; where the browser has no Popover API `<Popover>` falls back to a portalled one, plus `useDismiss` and `useFocusReturn`.
|
|
121
|
+
|
|
122
|
+
30. **Every floating layer is in the top layer now, and none of them is portalled.** `Overlay` — and so `Tooltip`, the `Dropdown` popup and the DataGrid column menu, which are all built on it — renders where it is declared and carries `popover="manual"`, shown as soon as it mounts. `manual`, not `auto`, because a layer owns no dismissal and light dismiss is a pattern: `<Popover>` is the `auto` one (rule 29), and `Tooltip` and `Dropdown` each keep the stricter dismissal they already implement — a tooltip's Escape has to _last_ (WCAG 1.4.13), and a JS-toggled trigger fights light dismiss. Four things follow, all measured in Chrome 152. The layer beats a `z-index: 9999` sibling and a transformed ancestor, where a plain `position: fixed` layer at the same coordinates loses both. It **inherits** the theme, the custom properties and the direction around it, so a local `Box.Theme` finally reaches inside a dropdown and there is no `dir` to copy across. The tab order follows the markup, so a layer declared after its trigger is what Tab reaches next. And a press inside it is a press inside whatever popover it was _declared_ in, so **a `Dropdown` inside a `Popover` panel no longer dismisses the panel** — the bug a portal caused by moving the popup to the body, where every press in it read as outside. **The one cost, measured:** a layer in the top layer **keeps the side it chose when it opened** — Chrome re-evaluates `position-try-fallbacks` on scroll for an ordinary positioned element and never for a top-layer one, so a layer left open while the page scrolls does not flip when its side runs out of room and slides past the viewport edge instead. Every _open_ picks the right side, since a layer that mounts when it opens is laid out for the first time then; only a scroll _while_ open is affected, nothing but leaving and re-entering the top layer re-arms it, and the portal fallback (which measures) does not have it. It applies to `<Popover>` too. Close a layer if the page can scroll far underneath it. The trap is the other side of the same coin: **never declare a layer inside its trigger.** It used to be legal because the portal moved it before a browser saw it, and now a `role="listbox"` or a `role="menu"` inside a `<button>` is unreachable content — put it beside the trigger instead. `<Presence>` still owns the mount, so a closed dropdown renders none of its options and the exit still runs (a top-layer element transitions normally); where the browser has no Popover API the portal comes back, with everything it used to cost.
|
|
123
|
+
|
|
124
|
+
31. **A modal is `<Dialog>`, and the platform is the whole pattern.** `<Dialog trigger={(t) => <Button {...t}>Rename</Button>}><Dialog.Title>Rename this view</Dialog.Title><Textbox name="name" /></Dialog>` (`components/dialog`) is a real `<dialog>` shown with `showModal()`, which supplies **the top layer, a `::backdrop`, an inert page behind it, Escape, focus containment and focus return** — so the focus trap, the `aria-hidden` sweep and the `z-index` strategy every other library ships are all absent here. `<AlertDialog>` (a named export of the same module) is `role="alertdialog"`, always modal and never dismissed by a press outside, because a decision that can be clicked away is one the user did not make; Escape still closes it, and `initialFocus` is what puts focus on the least destructive action, which is APG's rule for it. **Rendering a `Dialog.Title` is what names the dialog** — it writes `aria-labelledby`, and a `Dialog.Description` writes `aria-describedby`; neither attribute is written when the part is absent, so `label`/`labelledBy`/`describedBy` are the answer for a dialog that shows no heading. It takes `modal` (default `true` — `false` is `show()`: no backdrop, nothing inert, and the UA positions it against its containing block), `dismissible`, `lockScroll` (default: whatever `modal` is) and `onOpenChange(open, { reason })` with the same four reasons `<Popover>` uses, `imperative` covering a `close()` call and a `<form method="dialog">` submit. **Four things to know, all measured in Chrome 152.** The dialog is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>` — same as `<Popover>`. A **close cannot be refused**: `cancel` is cancelable but the browser has already closed the dialog by the time `onOpenChange` runs, so `dismissible={false}` is how a decision is made unavoidable. A press outside is `closedby="any"`, written for you — and where the browser has not got it yet the press is measured against the dialog's own box, because **for a modal dialog the backdrop _is_ the dialog element** and every containment test calls a press on it inside. And the platform does **not** stop the page scrolling, which is why `lockScroll` exists at all: it is an `overflow: hidden` class on `<html>`, held by a counter so an inner dialog closing does not unlock the page under an outer one.
|
|
125
|
+
|
|
126
|
+
32. **A menu button is `<Menu>`, and a submenu is a popover nested in it.** `<Menu trigger={(t) => <Button {...t}>Actions</Button>}><Menu.Item onSelect={duplicate}>Duplicate</Menu.Item><Menu.Separator /><Menu.Sub label="Share"><Menu.Item>Copy link</Menu.Item></Menu.Sub></Menu>` (`components/menu`) is APG's menu button on the Popover API: `role="menu"`, `menuitem`/`menuitemcheckbox`/`menuitemradio` with `aria-checked`, `role="group"` around a titled section and `role="separator"` between them. The parts are `Menu.Item`, `Menu.CheckboxItem`, `Menu.RadioGroup` + `Menu.RadioItem`, `Menu.Group` (a `label` names the group, so its items are read as a set), `Menu.Separator` and `Menu.Sub`, which renders its own item — label, chevron, `aria-haspopup="menu"`, `aria-expanded` — and the menu beside it. **The platform's half is the top layer, light dismiss and the toggle**: Escape closes the innermost menu first, one layer per press, and a press outside closes the lot, with no portal and no `z-index` anywhere. **What is written here is the keyboard and the ARIA**: Down/Up wrap, Home/End, typeahead (a letter again cycles through the items starting with it), Right opens a submenu and Left closes it — the _reading order_, so the two swap in a right-to-left menu and the chevron turns round with them. **Four things to know.** A `disabled` item is `aria-disabled` and **stays focusable**, which is APG's rule: the `disabled` attribute would take it out of the keyboard's reach and silence it. **A command closes the menu and a state does not** — `Menu.Item` closes on select, `Menu.CheckboxItem` and `Menu.RadioItem` do not, so several boxes can be ticked in one visit, and `closeOnSelect` swaps either default. `onOpenChange(open, { reason })` adds **`select`** (an item was chosen — the transition worth telling apart) and **`tab`** to the four a layer always reports. And the menu is **always rendered**, closed being `display: none`, so the exit is a CSS transition and never `<Presence>`, the items render whether or not it has been opened, and the top layer's one cost applies to it as it does to `<Popover>` (rule 30): a menu keeps the side it opened on. Placement is `side`/`align`/`offset`/`flip` on both the menu and each `Menu.Sub`, whose defaults are `side="end"` and `offset={0}` so a submenu abuts the menu it came out of. Opening a submenu moves focus into it, hover included, which is what keeps the highlight and the keyboard in one place. The semantic `<menu>` element is **`MenuList`** from `components/semantics` now — `Menu` there is deprecated, because two exports of that name are one import away from the wrong component.
|
|
127
|
+
|
|
128
|
+
33. **Tabs are `<Tabs>`, and selection follows focus.** `<Tabs defaultValue="overview"><Tabs.List label="Project"><Tabs.Tab value="overview">Overview</Tabs.Tab></Tabs.List><Tabs.Panel value="overview">…</Tabs.Panel></Tabs>` (`components/tabs`) is APG's tabs pattern in four parts: `role="tablist"` named by the list's own `label`, `role="tab"` with `aria-selected` and `aria-controls`, and `role="tabpanel"` named by its tab and carrying `tabindex="0"` so its content is reachable. **One arrow key moves and selects** — APG's default; `activation="manual"` splits the two, so the arrows move and Enter or Space chooses, which is what a panel too expensive to render on the way past needs. **The keyboard is one tab stop**: Tab enters the list once, landing on the selected tab, and again leaves it for the panel, however many tabs it holds. Right/Left in a horizontal list, Down/Up in a vertical one, both wrapping unless `loop={false}`, Home/End to the ends, and the **off-axis pair is left to the page**, so a horizontal list does not eat a scroll. The arrows follow the **reading order**, so in a right-to-left page ArrowLeft is the _next_ tab, and a vertical list puts its indicator on the inline end (`be`, not `br`). **Four things to know.** A `disabled` tab is **not** selectable and the arrows step over it — the opposite of `Menu.Item` (rule 32), because selection follows focus here and a tab focus could reach but selection could not would leave the widget with no state to be in. **Only the selected panel is rendered**, so an unmounted panel costs nothing and gets an entrance from `startingStyle` for free but loses whatever state it held: `keepMounted` renders them all and hides the rest, the `hidden` attribute plus a `display: none` of its own, since every Box carries `display: block` and outranks the UA's `[hidden]` rule. **The tabs are read off the DOM**, not out of a registry, so a tab wrapped in a layout of your own, rendered from a list or put behind a condition navigates like any other, and a nested set of tabs belongs to its own list. And **the tab sequence follows the selection** — a controlled `value` changed elsewhere on the page moves the keyboard's entry point with it — except under `activation="manual"`, where it follows the focus, as APG's own example does. `onValueChange(value, { reason })` reports `'click'` or `'keyboard'`. **Two things move, and both are opt-in.** `indicator="sliding"` swaps each tab's own border for one element for the whole list, which animates between tabs because it _is_ the same element; wrapping the panels in a **`Tabs.Panels`** gives that container the height of the panel on screen, so panels of different heights transition instead of jumping. Both are measured, with the consequences that follow from that: the travelling indicator appears only once the widget has run, so until then the tabs keep drawing their own border — which is what a prerendered page paints, in the same place, so the handover is invisible; the container watches the **panel** and never itself, since its own height is what it writes; and both ride `--transitionTime`, so `prefers-reduced-motion` computes each to `0s`. The container is clipped **only while its height is travelling**, because at rest it is exactly as tall as its panel and a permanent clip would cut the focus ring off anything sitting at that edge — and `overflow` would clip both axes whatever was asked for, computing a `visible` companion to a clipped axis up to `auto`. Styling is the `tabs`/`tabs.list`/`tabs.tab`/`tabs.panel` tree plus `tabs.indicator` and `tabs.panels`, where the selected state is the bare `selected` key (it is `aria-selected`); **on a tab it is `ariaAttr={{ selected: … }}`**, because a top-level `selected` _prop_ writes that attribute rather than nesting styles under it — the pseudo2 rule every one of `selected`/`disabled`/`checked` follows. The indicator is a border rather than a background, because a forced-colors mode keeps one and throws the other away.
|
|
129
|
+
|
|
130
|
+
34. **An accordion is `<Accordion>`, and its height animation is a class.** `<Accordion defaultValue={['shipping']}><Accordion.Item value="shipping"><Accordion.Trigger>Shipping</Accordion.Trigger><Accordion.Panel>Two to four days.</Accordion.Panel></Accordion.Item></Accordion>` (`components/accordion`) is APG's accordion in three parts, and **`Collapsible` is a named export of the same module** — one disclosure with a render-prop `trigger` and no heading, the way `<Popover>` reads. **Nothing is measured.** The panel sits in a grid of one row whose track runs `1fr` to `0fr`, so there is no `ResizeObserver`, no measured pixel and no custom property per instance: two shared rules animate every accordion on the page, and content that grows while a panel is open grows with it. **A closed panel is hidden with `visibility`, and is always rendered** — content behind a zero-height track is still laid out, still tabbable and still read out, and `visibility` is the one hiding mechanism that is _animatable_, flipping to hidden only once the track has closed and back the instant it opens. That is why there is no `@starting-style` and no `transitionBehavior="allow-discrete"` here, and why a server-rendered open panel does not animate itself open on load; it also means a half-filled form in a panel survives being shut, and expensive children are yours to gate with `{open ? … : null}`. **Four things to know.** **Every header is its own tab stop** — the opposite of `Tabs` (rule 33), because an accordion is not a composite widget: Down/Up (wrapping unless `loop={false}`) and Home/End are a shortcut between headers, and the sideways pair is left to the page. Each header is a real `<button>` inside a heading whose level is **`level`** (default `3`), since a heading level is the document's outline; the panel is a `role="region"` named by it, and `props={{ role: undefined }}` drops the landmark past the half-dozen panels APG's own caveat warns about. **One panel at a time unless `multiple`**, and closing the open one is always allowed, so there is no second prop for whether the accordion may stand empty — `value`/`defaultValue` are a `string[]` either way. A `disabled` item is the **`disabled` attribute** and the arrows step over it, as on a tab. Styling is the `accordion`/`accordion.item`/`accordion.heading`/`accordion.trigger`/`accordion.arrow`/`accordion.panel` tree plus `collapsible`, and a header's open state is its own `aria-expanded`, so `ariaAttr={{ expanded: … }}` styles it with no variant. The two parts that are not yours are **`accordion.track`**, the grid whose row animates, and **`accordion.clip`** inside it, the bare item that does the clipping. Bare is the point: **padding cannot be squeezed**, so a grid item carrying any floors the `0fr` track at exactly that much — which is why the panel you pad sits _inside_ the clip rather than being it (measured, #142).
|
|
131
|
+
|
|
132
|
+
35. **A slider is `<Slider>`, a bar is `<Progress>`, and a number in is a number out.** `<Slider label="Volume" defaultValue={40} onValueChange={(value) => setVolume(value)} />` (`components/slider`) is APG's slider, and `defaultValue={[20, 80]}` with `thumbLabels={['Lowest', 'Highest']}` is the multi-thumb one — **the value's own shape is how many thumbs there are**, so there is no second prop saying which kind this is and no narrowing at the call site: a `number` in means a `number` back, an array means an array. A thumb may meet its neighbour and never pass it. `<Progress label="Upload" value={62} />` (`components/progress`) is the `role="progressbar"` beside it, and **it renders on a server** — no state, no effect, no measurement. **Neither is the native element, and that is the one place the platform loses**: `<input type="range">` holds a single thumb, and it and `<progress>` both draw themselves with vendor pseudo-elements (`::-webkit-slider-thumb`) that no typed prop can reach — so `name` on a `Slider` is what keeps the form working, writing one hidden input per thumb. **Four things to know.** **The position is an inline style, and it is the only one**: a thumb's offset is per _frame_ of a drag, so a class for it would be a rule per frame that is never freed — the exception `useAnchorPosition`'s anchor name and the travelling tab indicator already take, and the reason a `ProgressRing` (`components/chart`) can round its fraction into a class instead, since nobody drags a ring. **No `value` on a `Progress` is a state, not a zero**: `aria-valuenow` is omitted rather than reported as `0`, and the bar sweeps — a named duration, so it stops itself under `prefers-reduced-motion`. **A `role="slider"` has to be named**: `label` names the thumb on a single slider and the `role="group"` on a range, where `thumbLabels` names the thumbs one at a time, and `format` writes `aria-valuetext` for a value a bare number does not read as. And **it mirrors for free** — the fill and the thumbs are placed with `inset-inline-start`, so a right-to-left page draws the minimum on the right with nothing declared twice; only the sideways arrows swap (Left is the increase), and Up/Down never do, because the block axis has no reading order. `onValueChange` fires on every step and `onValueCommit` once at the end, both with `{ reason: 'pointer' | 'keyboard' }`. **A press travels and a nudge does not**: a press on the track is the one move the eye has to follow, so the thumb animates the whole way, while every other move — a drag, an arrow key, a held arrow — takes a short 60ms **`ease-out`** travel on both the thumb and the fill, which is the `tracking` variant and which both parts must carry or they come apart. Short rather than _off_, because off is exact and **steps**: a value on a grid can only be at its grid positions, so a 1-in-100 slider moves 3.2px at a time on a 320px track. Eased **out** rather than linear or eased, because a repeat restarts the transition every 33ms so only its first half is ever seen — on this curve that half is the straight part, so it glides while the key is held and still decelerates on the last transition, the only one allowed to finish; plain `ease` replays its slow-in and pulses, and `linear` rides more than a full step behind the value and stops at full speed. And an arrow key is a nudge rather than a jump, so it never takes the press travel at all — one tap spending 250ms on 3.2px was itself the lag. `step` is the real dial — `step={0}` is continuous and needs no interpolation at all. Styling is the `slider`/`slider.track`/`slider.fill`/`slider.thumb` tree and `progress`/`progress.fill`, whose `indeterminate` variant is the sweep; `disabled` is `aria-disabled` here rather than the attribute, since a div takes none and a value nobody can reach is a value nobody can read.
|
|
133
|
+
|
|
134
|
+
36. **A message you send rather than render is `<Toaster>` plus `toast()`.** `<Toaster />` once near the root of the app (`components/toaster`), and after that `toast.success('Saved')` from anywhere at all — an event handler, a fetch, a router guard, a module with no React in it. The store behind it is framework-free, so a call made **before** the viewport mounts is queued rather than lost, and the component only draws what is there. The surface is `toast(message, options)` with `success`/`error`/`warning`/`info`/`loading` beside it, `toast.promise(p, { loading, success, error })` (one toast for the whole call, and the promise comes back untouched), `toast.update(id, message, options)` and `toast.dismiss(id?)`; an `options.id` already on screen is an **update**, which is what makes a promise one toast rather than two. A toast takes a `description`, one `action` (`{ label, onClick }`, which dismisses the toast it answered unless `closeOnClick: false`), a `duration` in milliseconds (`Infinity` waits, and `loading` already does), and `onDismiss(reason)` with `timeout`/`close`/`action`/`imperative`. **The limit is a queue, not a cap**: past `limit` (default 3) a toast waits **with its timer unstarted**, so nothing expires that was never on screen, and a counter at the far end of the stack says how many are waiting. **Four things to know, all measured in Chrome 153.** The viewport is a **polite live region that exists before there is anything in it** — a region inserted together with its content is not reliably announced — and an error toast is `role="alert"`, the one announcement pattern every screen reader implements; nothing else carries a region of its own, because the nearest region to a change is the one that speaks. It is in the **top layer** with no portal (`popover="manual"`, shown as it mounts), so it inherits the theme and the direction around it, and it takes **no pointer events at all** while the toasts take them back — a corner-sized fixed strip would otherwise swallow every press in its gaps. **Timers stop on hover, on focus and in a background tab** and pick up where they left off, which is WCAG 2.2.1 and the only reason a toast may carry a control at all; a toast **never takes focus**, so `F6` (or `hotkey`, which takes `'alt+t'` or `false`) is what moves focus to the stack, Tab walks it, and Escape dismisses the toast focus is in and hands focus back once the stack is empty. And the viewport has to answer the UA's own `[popover]` rule: `inset: 0` leaves a corner-pinned stack over-constrained — `top`/`left` win and it sits in the wrong corner — and `overflow: auto` makes it a scroll container, so the style tree declares all four sides `auto` and `overflow: visible` before a `position` variant names the two it wants. `position` is `'bottom-end'` by default and its inline half is logical (`start`/`end`), the newest toast is always nearest the screen edge, and styling is the `toaster`/`toaster.toast`/`toaster.message`/`toaster.description`/`toaster.action`/`toaster.close`/`toaster.overflow` tree, where the kind is a variant on the toast.
|
|
135
|
+
|
|
136
|
+
37. **A DataGrid cell is edited by two props, and one of them judges the edit.** `column.editable` (`true`, or a predicate over the row) is the whole opt-in — `def.editable` is the grid-wide default a column falls back to — and **`def.onCellEdit({ rowKey, columnKey, row, value, oldValue })` both validates the edit and is told about it**: answer nothing to accept the value, a string to refuse it with that message, `false` to refuse it with the grid's own. It may return a **promise**, so a uniqueness check against a server is the same function; an answer to an edit the user has since abandoned is dropped rather than applied to whatever they are editing now, and a rejected promise is its message. **Which editor a cell opens is read off the value in it** — a number gets `number`, a boolean `checkbox`, everything else `text` — so a column of numbers has a numeric keypad without being told; `column.editor` names one of the four (`text`/`number`/`checkbox`/`select`) or configures it (`{ type, options?, placeholder?, step?, min?, max? }`, where `options` may be a function of the row), and `column.EditCell` is a control of your own bound to `cell.draft`, with `cell.setDraft(v)`, `cell.commitEdit()` and `cell.cancelEdit()` beside it — the same three calls the built-in four make, so a date picker is a component rather than a second copy of the commit rules. **A press chooses and a double press opens**: clicking a cell makes it the current one — the cell the arrows carry on from, marked whether the pointer or the keyboard put it there — and double-clicking an editable one opens its editor, the way a spreadsheet reads a double press; a cell that cannot be edited takes both as focus and nothing else, and a double press landing on a widget inside a cell (a tree chevron, a link in a `Cell` of your own) belongs to the widget rather than the editor. **The keyboard is APG's**: Enter or F2 opens the editor, any printable character opens it on that character (replacing the value, the way a spreadsheet reads a keystroke), Escape throws the draft away, Enter commits and hands the keyboard back to the cell, a press elsewhere commits, and **Tab commits and opens the next editable cell** — along the row and on into the rows after it, so a row of corrections is one gesture; `checkbox` and `select` commit on the change instead, since one interaction is the whole value. **Four things to know.** A **refused value keeps the editor open and the caret in it**, with the message in a `role="alert"` **in the top layer** — a bubble inside the scroller would be clipped away on the last row, which is the row a long grid is most often edited on — plus `aria-invalid` and `aria-describedby` on the field; typing clears it. **The grid does not own `data`, so an accepted value is kept as an edit _over_ the rows it was given** and is what every cell, every `Cell` renderer and every export reads from then on — which is what lets a `def.dataSource` grid be edited at all, since a value that was only reported would be gone on the next block. **`onCellEditsChange(edits, { reason })` reports the whole list, oldest first** — the stream an undo is built out of — and **`clearEdits()` on the grid's ref** is how a host says its own data has caught up; a datasource `refresh()` drops them with the blocks, because what the server says next is the newer answer. And **an edit never re-sorts or re-filters the grid**: sorting and filtering read the row's own values and only what is _displayed_ reads the edit, so a row cannot jump out from under the person typing into it. Styling is `datagrid.body.cell`'s `isEditing` and `isInvalid` variants — one ring, red when the value was refused, because two rings on the same rectangle means the editing one paints over the other — plus `datagrid.body.cell.editor` (`isPending`) and `datagrid.body.cell.error`.
|
|
137
|
+
|
|
138
|
+
Full reference: `references/` beside this file, and `docs/props.md` inside the package.
|