Appearance

A design for the whole site, in light or dark. Kept on this device.

Design
Mode

Box Kite 2.3.1

6 November 2026
What changed in this version, written as it landed. The same notes are the body of the GitHub release.

6 November 2026 · npm · Compare v2.3.0...v2.3.1

An SVG shape can now turn about its own centre, take a press on its stroke alone and keep a finger's drag, and the SVG geometry props draw in Firefox and Safari. A large DataGrid also scrolls with no blank frames, re-renders far less, and numbers its rows correctly inside nested and server-side groups.

An SVG shape turns about its own centre, an edge takes the press on its stroke, and a drag keeps the finger

Three new props, and three that take more values. Everything below was checked in Chrome, Firefox and Safari's engine.

  • transformOrigin and transformBox. An SVG shape measures its origin against the whole viewBox, so rotate={45} on its own swings it round the corner. With transformBox="fill-box" and transformOrigin="center" it turns about its own centre. transformOrigin takes a keyword ('center', 'top left'), a fraction or a percentage, a number on the ÷4 scale, or two or three positions as CSS writes them ('48px 48px').
  • touchAction: auto, none, manipulation, pan-x, pan-y, pinch-zoom and their combinations. Without touchAction="none", a finger dragging an element pans the page instead of moving the element.
  • pointerEvents takes the SVG keywords: visiblePainted, visibleFill, visibleStroke, visible, painted, fill, stroke and bounding-box (Firefox doesn't support bounding-box and uses the default). Put visibleStroke on a wide, transparent stroke and only the line takes the press, not the area it encloses.
  • paintOrder takes up to three keywords in the order they paint ('stroke fill markers').
  • strokeDasharray="dashed" and "dotted" grow with the line. strokeWidth now also sets --boxStrokeWidth, and the two named patterns are multiples of it, so a thicker line keeps the same pattern.
JSX
<Svg viewBox="0 0 200 200" width={200}>
  <G rotate={45} transformBox="fill-box" transformOrigin="center">
    <Rect x={120} y={120} width={40} height={40} fill="red-500" />
  </G>
  <Path d="M20 180 L180 20" stroke="transparent" strokeWidth={16} pointerEvents="visibleStroke" touchAction="none" />
  <Line x1={0} y1={190} x2={200} y2={190} stroke="slate-500" strokeWidth={2} strokeDasharray="dashed" />
</Svg>

Breaking changes

None.

Fixes

  • The SVG geometry props drew nothing in Firefox and Safari. cx, cy, r, rx, ry, x and y were written as bare numbers (r: 20), which only Chromium accepts in CSS, so a ProgressRing or a MiniDonut had no radius there. They are written in px now (r: 20px), which in SVG is the same user unit, so nothing changes in Chromium.

  • A DataGrid grouped two levels deep numbered its rows wrongly once an inner group was open — the row-number column repeated and skipped numbers, and every "Select row N" label followed it. The rows are now numbered in the order they appear, group rows included, with inner groups open or shut in any combination; copying a group row's row-number cell copies the number it shows instead of a blank.

  • Resizing a DataGrid column widened the row number, row selection and row detail columns. The space a resize freed was shared out to them along with the data columns, so the checkbox column grew with every drag. Those three now keep their own width, and the space goes to the data columns.

  • A fast wheel scroll through a DataGrid showed blank body. About one frame in five had rows missing under 800px wheel steps, because the rows a scroll revealed were drawn a frame after the scroll. They are now drawn in the same frame: measured on the docs demo, 0 blank frames where there were 19–21%.

  • After a filter, the DataGrid's scrollbar thumb slid to its new size instead of jumping there. The spacers that give the scrollbar its range picked up every Box's 250ms transition, so a reader scrolled deep was dragged back over a quarter of a second. They resize at once now, and a scroll left past the end of a shortened list draws its last rows rather than an empty window.

  • Every DataGrid cell's text was a scroll container, which could draw a stray scrollbar on Windows and catch the mouse wheel when a cell's content overflowed. It clips instead, as the grouping cell of a group row now does too.

  • Opening a detail panel or a group in a large DataGrid re-filtered and re-sorted every row. Opening a row changes which rows are shown, not which rows there are, so it now only works out the new list. A detail panel in a sorted grid of 100,000 rows opens in about 5ms instead of about 210ms.

  • A DataGrid whose def is written inline re-filtered and re-sorted every row on every render of its parent. A def built in the render body is a new object each time, and that rebuilt everything behind the rows. Filtering, sorting and the row models are now kept on the values they actually read, so a parent re-render costs about 2.5ms on 100,000 sorted rows instead of about 47ms. Keeping def at module scope is still the cheapest choice.

  • Resizing the window re-rendered every row of a DataGrid on every frame, and so did letting go of a column's resize handle. The rows take their widths from CSS variables that are already written straight to the grid, so now only the header redraws. On the docs demo, the script work in each frame of a window drag is about half what it was.

  • A DataGrid re-rendered whenever the component holding it did, even with the same props. It is memoized now, and its totals row no longer redraws on every scroll frame.

  • A DataGrid fed by a dataSource restarted its row numbers inside every open group, and inside every open row of a lazy tree — a row's number was its place in the block the server sent. It is now its place on screen, the same count a grid grouped in the browser gives, and it follows when a group above it opens or shuts.