react-x11 2.52.0 → 2.53.0

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.
Files changed (112) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1091 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
@@ -1,255 +0,0 @@
1
- # Frame pacing: a budget over paint time, not a count of updates
2
-
3
- _Design record, 2026-09-07. Written against master at 25d6869 (react-x11
4
- 2.8.3, ntk ^8.7.0, `@windowkit/appkit` ^0.6.0), for the `frameRate` prop
5
- that shipped with it. The user-facing account is
6
- [elements.md](../elements.md#framerate--pacing-the-frames-under-a-flood);
7
- this is why it is shaped the way it is, where it sits, and what it
8
- measured. The problem was found in `@react-x11/components`'s vt terminal
9
- (sidorares/react-x11-components#69, "PRD: adaptive frame pacing"), whose
10
- §6 lists what core would have to grow; this document is core's half of it,
11
- made generic._
12
-
13
- ---
14
-
15
- ## 0. TL;DR
16
-
17
- - A window fed off an input path — a terminal under `cat`, a chart on a
18
- socket, a simulation, a scene drawing as fast as it can — claims a repaint
19
- every time its data moves. The frame clock decides _when_ a frame may go
20
- out and says nothing about whether it is worth painting, so on a 120Hz
21
- panel such a window paints 120 times a second. Where a frame costs
22
- milliseconds of the JS thread (full-window CoreGraphics on macOS), the
23
- thread paints screens nobody sees and the producer gets what is left:
24
- measured on a streaming terminal, 60% of wall time in paint and a flood
25
- four times slower on macOS than under XQuartz on the same machine.
26
- - The fix prices frames in **CPU time**, not in updates: a token bucket
27
- over paint time. Credit accrues at `budget` per millisecond of wall time
28
- up to a burst; a frame spends what it measurably cost; a claim that finds
29
- the bucket in debt waits for it to reach zero and no longer. Idle is
30
- immediate, cheap is unthrottled, one expensive frame among cheap ones is
31
- free, and a flood that ends is un-throttled on its next claim.
32
- - It lives **above both frame clocks**, in `WindowNode._scheduleFrame` and
33
- `GlAreaNode.requestFrame`: a held claim delays the `requestAnimationFrame`
34
- request, and the clock — ntk's fence or vertical blank on X11, the display
35
- period on macOS — still gates the frame itself. Nothing changes what a
36
- frame paints.
37
- - **Opt-in.** `'display'` — every frame the clock gives, after React's own
38
- batching — is the default; `'adaptive'` is what a streaming window asks
39
- for, on the `<window>` or `<glarea>`, for a root, or from
40
- `REACT_X11_FRAME_RATE`.
41
- - Beside it, `Node.opaqueRect()`: an element that composites an opaque
42
- bitmap over its box says so, and a pass inside it skips the fills that
43
- were under it — the other fifth of the terminal's frame.
44
-
45
- ## 1. The problem, measured
46
-
47
- The components PRD's numbers, on an M1 Pro with a 120Hz panel, a 1000×700
48
- window at 2×, a 125×45 terminal grid and 300,000 distinct lines `cat`-ed
49
- into its pty:
50
-
51
- | variant | flood | paints/s | paint share |
52
- | --------------------------------------------- | -----: | -------: | ----------: |
53
- | Cocoa, defaults | 3.50 s | 110 | 60% |
54
- | Cocoa, `cocoa: { frameInterval: 33.3 }` | 1.08 s | 34 | 21% |
55
- | X11 (XQuartz), defaults | 0.85 s | 35 | 7% |
56
- | Cocoa, an element-owned pacer at a 25% budget | 0.95 s | 35 | 20% |
57
-
58
- Three facts fall out of it. **Frame count is the lever**: both backends
59
- run the clock at the display's period, but the X11 frame is _fenced_ — ntk
60
- does not start the next until the server has consumed the last, and the
61
- server takes ~28ms to composite a screenful of glyphs, so the terminal
62
- paints 35 times a second by backpressure. The Cocoa backend has no
63
- equivalent: its "server" is CoreGraphics on the JS thread and the frame is
64
- due whenever the display is. **Per-frame cost is full-area memory passes,
65
- not glyphs**: of a 6ms Cocoa frame, 2.0ms is the cell backgrounds, 1.7ms
66
- the surface-to-window composite, 0.95ms the swapchain catch-up copy,
67
- 0.7ms two background fills under the opaque surface — and 0.26ms the
68
- glyphs. **A fixed cap is the wrong tool**: `frameInterval: 33.3` recovers
69
- most of the gap and costs every window on the root its 120Hz, including
70
- the ones whose frames are cheap.
71
-
72
- ## 2. The rule
73
-
74
- The PRD's rule was "defer a claim until `lastPaintEnd + cost × (1/budget −
75
- 1)`", from the last frame's cost. That is the right steady state and the
76
- wrong transient: it holds the frame after a single expensive one — the one
77
- relayout in a blit-scrolled list — for three times its cost, on a scroll
78
- that should stay at 120Hz because everything else in it is a memmove and a
79
- strip. The user's requirement was exactly that case: _budget CPU price,
80
- not the number of updates; a scroll that is mostly blits stays at 120Hz
81
- with rare repaints._
82
-
83
- The token bucket is the same rule with a burst (`src/pacing.js`,
84
- `FramePacer`):
85
-
86
- ```
87
- credit += budget × (now − lastAccrual), capped at burst
88
- frame: credit −= cost // what the flush measured
89
- claim: wait = credit ≥ 0 ? 0 : −credit / budget
90
- wait = min(wait, lastEnd + 1000/minFps − now) // the floor
91
- wait = max(wait, lastStart + 1000/maxFps − now) // the ceiling
92
- ```
93
-
94
- - With the bucket at zero, a frame of cost `c` leaves it at `−c(1 −
95
- budget)` and the next claim waits `c(1/budget − 1)` — the PRD's rule, in
96
- steady state under a flood. Over a long stream the paint share converges
97
- on the budget exactly (`test/pacing.test.js`, "converges on the budget").
98
- - The **burst** is what absorbs the rare expensive frame: a single frame up
99
- to the burst never earns a wait, and only a stream drains it. The burst
100
- is the floor's interval (50ms at `minFps: 20`), 50ms without a floor —
101
- one frame up to the longest wait the pacer may impose is always free.
102
- - The debt is bounded by one frame's cost, so a flood that ends is
103
- un-throttled after that one wait. An average would have kept throttling
104
- the prompt that appears when the flood stops.
105
- - Waits under a millisecond are not armed: a timer cannot keep them and a
106
- clock tick would swallow them. The debt carries to the next claim, so a
107
- run of sub-millisecond frames claimed back to back still pays once it is
108
- worth a timer, and a cheap frame after a flood lands on the tick it would
109
- have landed on anyway.
110
-
111
- **Cost is the JS thread's time.** The flush — layout, the paint passes, the
112
- requests or the CoreGraphics work — bracketed in `WindowNode.flush`, plus on
113
- macOS the present that runs after it (`CocoaWindow.present` reports the
114
- flip and its catch-up copy back to the node). Not the server's: on X11 the
115
- fence already paces to that, and the pacer is deliberately inert where a
116
- backend has backpressure of its own. The PRD's Q5 asked whether a core seam
117
- reporting the whole flush's cost would make the rule exact on both
118
- backends; the answer is that the thread's time _is_ the cost the rule is
119
- about, because it is what the producer is starved of.
120
-
121
- ## 3. Where it lives, and what it composes with
122
-
123
- The pacer is per frame source: one on each `WindowNode` (popups included)
124
- and one on each `GlAreaNode`, whose frames run on a clock of their own.
125
- `_scheduleFrame` asks it before requesting the window's callback; a claim
126
- raised by the frame on itself — an animation stepping, a container query
127
- settling, a promotion moving a node — is answered once the frame is over,
128
- so the frame that raised it is the one priced.
129
-
130
- - **The clocks.** A held claim delays the `requestAnimationFrame` request
131
- by the wait; the clock then gates the frame as it always did. On macOS a
132
- request between two pump ticks used to wait for the next tick to look at
133
- it, up to a pump interval on top of the wait; `CocoaApp._requestFrame`
134
- now arms the same one-shot a tick arms for a frame it just missed, at the
135
- moment the clock is due, so a paced claim lands at its wait rather than
136
- its wait rounded up to the pump. On X11 ntk's clock is untouched.
137
- - **Discrete input.** A click or a key paints the window at once
138
- (`flushPendingFrames`, [events.md](../events.md)), held claims included —
139
- a held claim is damage like any other, and the early flush stands the
140
- wait down. The debt is paid then. Answering the input is never held.
141
- - **Animations.** A transition's frames go through the same claims and are
142
- paced like any other; at the default nothing changes, and under
143
- `'adaptive'` cheap transition frames never wait. A layer promotion's
144
- animations run in the render server and schedule no frames at all.
145
- - **Hidden and occluded windows** schedule no frames; the bucket refills
146
- while they wait, so the catch-up frame is immediate.
147
- - **`cocoa: { frameInterval }`** is still the clock's own cap on macOS and
148
- applies to everything on the clock; `frameRate` is the policy above it,
149
- per window, on both backends. `maxFps` is the cap an app should write.
150
-
151
- ## 4. The API
152
-
153
- `frameRate` on `<window>`, `<popup>` and `<glarea>`; `createRoot({
154
- frameRate })` for a root's default; `REACT_X11_FRAME_RATE` over both. A
155
- preset name, a number (a ceiling and nothing else), or `{ budget, minFps,
156
- maxFps }` with the ones left out taken from `'display'`. A `<glarea>` takes
157
- its window's policy unless it names one. Resolved in `src/pacing.js`
158
- (`resolveFrameRate`, `resolveFramePolicy`), validated at the call that
159
- passed the value — a bad root option fails at `createRoot`, a bad prop at
160
- the window — with the choices in the error.
161
-
162
- Decided, and why (the PRD's §11, answered for core):
163
-
164
- - **The name** is `frameRate`: it is what an author thinks in, and the
165
- numeric form reads as a cap.
166
- - **The environment overrides the prop**, as `REACT_X11_BACKEND` does: an
167
- A/B run and a field diagnosis must not need a code change in every app.
168
- - **A number is a ceiling over the default**, not over `'adaptive'`. With
169
- `'display'` the default, `frameRate={30}` has to mean "never more than
170
- 30" and nothing else; adaptive-and-capped is spelled with the numbers.
171
- - **The default is `'display'`.** A UI answers its input as quickly as it
172
- can unless it says otherwise; the pacer is what a window that streams
173
- asks for. It costs the default path one property read per claim.
174
- - **Several windows pace themselves.** Four floods at once can take four
175
- budgets; a shared budget is a process-level clock and is not built.
176
-
177
- Observability: `REACT_X11_TRACE=requests` prints `waited=` on every frame
178
- the pacer held ([debugging.md](../debugging.md)), the `frame` trace hook
179
- carries it, and the pacer's counters — frames, held, coalesced, the last
180
- cost and the last wait — are on the node for a bench to print.
181
-
182
- ## 5. The opaque-content hint
183
-
184
- Of the terminal's 6ms frame, 0.7ms was two background fills under a
185
- surface that covers every pixel of them, and on X11 the same fills are
186
- composites the server ran for nothing. Core had no way to know.
187
- `Node.opaqueRect()` is the element's word: the rect it writes opaque pixels
188
- over on every paint, in whole pixels; a pass inside it is painted without
189
- the clear, the window's background, and the backgrounds of the node and its
190
- ancestors. Clipping ancestors shrink the cover to what reaches the surface,
191
- a rounded one by its radius all round. The element claims its damage as a
192
- rect inside the answer, because a node claim carries a pixel of slop that
193
- lies outside it ([extending.md](../extending.md#an-element-that-covers-its-box)).
194
- Pinned by `test/opaque-content.test.js`: the fills skipped are exactly
195
- those, and a covered pass leaves the pixels a full repaint leaves.
196
-
197
- ## 6. What is not here, and why
198
-
199
- Two of the PRD's four seams need the native bridge first, and the third
200
- needs a design of its own. Each is filed rather than folklore:
201
-
202
- - **`globalCompositeOperation` on the Cocoa context, with a memcpy for
203
- `'copy'`** (the 1.7ms surface-to-window composite). `@windowkit/appkit`
204
- 0.6.0 has no blend-mode verb, and its one memcpy, `copySurfaceRegion`,
205
- refuses surfaces of unequal size — it is the swapchain's catch-up copy,
206
- not a blit. Both verbs are the bridge's to grow; the property is declared
207
- optional on `Context2D` in the meantime, so an element asks for it
208
- rather than assumes it.
209
- - **Element-owned layer contents** (`presentSurface()` on a node over an
210
- IOSurface-backed `Surface`, so the terminal's surface _is_ its layer and
211
- the present and the swapchain copy disappear). The bridge has the
212
- primitives — `createSurfaceIOSurface`, `setLayerContentsIOSurface` — but
213
- a layer showing an IOSurface the element then draws into again tears; it
214
- needs a two-buffer chain of its own, a flip on present, and the promotion
215
- policy of `src/cocoa/promotion.js` extended to "this node presents a
216
- surface" with the same z-order rule. A design document first.
217
- - **An adaptive tick-skip in the Cocoa clock.** Not needed: with the pacer
218
- above the clock the clock has no tick to skip — a window that is holding
219
- a claim has no callback queued.
220
-
221
- ## 7. Measured
222
-
223
- `npm run bench:presenters -- --scenario=stream` is the flood in core's own
224
- terms: a window-sized element that answers `opaqueRect()`, painting a
225
- 120×40 grid of cell fills, claimed every 2ms from a timer — the producer —
226
- with `--frame-rate` choosing the policy. Read `cpu` and the paint share
227
- against `claims/s`, which is how often the producer got the thread.
228
-
229
- On the M1 Pro's 120Hz panel, a 900×700 window at 2× (the stream pane is
230
- the window less a header), six seconds a cell, `--columns=surface`:
231
-
232
- | `--frame-rate` | fps | flush avg | paint share of wall | cpu | producer: claims/s |
233
- | -------------- | ----: | --------: | ------------------: | --: | -----------------: |
234
- | `display` | 120.0 | 4.84ms | 58% | 72% | 244 |
235
- | `adaptive` | 36.1 | 5.79ms | 21% | 27% | 529 |
236
- | `30` | 29.7 | 6.47ms | 19% | 24% | 539 |
237
- | `throughput` | 8.8 | 10.26ms | 9% | 13% | 592 |
238
-
239
- Read across a row. At the default the window paints every refresh and the
240
- thread spends 58% of its time doing it; the producer, a 2ms timer that
241
- would fire 500 times a second on an idle thread, fires 244 — the flood is
242
- starving the thing that feeds it, which is the whole finding. Under
243
- `'adaptive'` the pacer holds 330 of 341 claims (the first eleven are the
244
- burst), paints 36 times a second, keeps paint at the budget — 21% where
245
- the rule says 25%, the difference being the clock rounding each wait up to
246
- the next tick — and the producer gets 529 of its 500-per-second, which is
247
- to say all of it. The cap at 30 lands where it says. `'throughput'` is the
248
- other end: 9% of the thread, one frame every 100ms. The per-frame cost
249
- rises as frames get rarer (a cold cache, a core the OS clocks down between
250
- frames), which is why the row is priced in share and not in milliseconds.
251
-
252
- What the table does not show is as important: a scroll under `'adaptive'`
253
- paints at 120Hz like the default, because its frames cost a fraction of a
254
- millisecond (`test/frame-pacing.test.js`, "cheap frames never wait"), and
255
- a click during the flood is answered on the click.
@@ -1,358 +0,0 @@
1
- # Grid layout: CSS grid on the layout seam
2
-
3
- _Design record, 2026-09-11. It began the same day as a feasibility study
4
- with a prototype, on the branch that added the layout seam
5
- ([custom-layout.md](custom-layout.md)); this is the record of what was
6
- built from it. Measured in the in-process X server with real text shaping
7
- (KaTeX Main), on one machine (Apple M1 Pro, node 26), and against Chrome 152. The outside facts in §8 are as of the date and linked; they will
8
- drift._
9
-
10
- _[styling.md](../styling.md#grid) is the reference for the style
11
- properties; this is why they are the shape they are._
12
-
13
- ---
14
-
15
- ## 0. TL;DR
16
-
17
- - **What:** `display: 'grid'` lays a box's children out by CSS Grid's
18
- placement algorithm (css-grid-2 §8.5) and its track sizing algorithm
19
- (§12), inside the layout pass, as a layout on the seam. `layout: 'grid'`
20
- is the same request; a box whose `display` and `layout` disagree is
21
- reported and laid out as flexbox.
22
- - **The style is CSS's, and flat** (§2): `gridTemplateColumns`,
23
- `gridTemplateRows`, `gridTemplateAreas`, `gridAutoColumns`,
24
- `gridAutoRows`, `gridAutoFlow` and `justifyItems` on the box; `gridColumn`,
25
- `gridRow`, `gridArea` and `justifySelf` on a child. The names every
26
- CSS-in-JS library and React DOM write, and the ones yoga's own grid uses.
27
- - **Held to Chrome** (§4): a 400-case corpus Chrome laid out is part of the
28
- test suite and lays out here within a pixel of it, but for one case where
29
- Chrome parts ways with the spec and this grid keeps to the spec.
30
- - **Cheaper than the flexbox that does the nearest thing** on every frame
31
- measured (§3) — 300 cards: mount 59 ms against 63, one card's text
32
- changing 2.1 ms against 13.1, a live resize 20.3 ms against 25.8.
33
- - **Left out** (§5): subgrid, which this seam cannot carry; baselines;
34
- named lines; the `grid` and `grid-template` shorthands; the second column
35
- pass.
36
-
37
- ## 1. What grid is for here
38
-
39
- Flexbox lines things up in one direction. The layouts an app here reached
40
- for and could not write:
41
-
42
- - **A form.** Labels in a column as wide as the widest label, fields taking
43
- the rest. Flexbox did it with a fixed label width, which a translation
44
- breaks, or by measuring in an effect, a frame late
45
- ([custom-layout.md](custom-layout.md#1-why-the-core)).
46
- - **A dashboard.** Tiles spanning columns and rows, packed densely.
47
- - **A gallery.** As many columns as fit, each row as tall as its tallest
48
- item. `flexWrap` nearly does it, but the last row's items stretch across
49
- the whole width.
50
-
51
- Those are the three cases measured in §3.
52
-
53
- ## 2. API
54
-
55
- ```jsx
56
- <box
57
- style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: 12 }}
58
- >
59
- <text>Name</text>
60
- <textinput />
61
- <box style={{ gridColumn: '1 / -1' }} />
62
- </box>
63
- ```
64
-
65
- ### Flat, and CSS's names
66
-
67
- The study proposed the shape a registered layout has —
68
- `layout: { name: 'grid', columns: 'auto 1fr' }` on the box and
69
- `layoutItem: { column: 'span 2' }` on a child — and it read as heavy,
70
- because it is the one thing nobody else does. The prior art (§8) splits in
71
- two:
72
-
73
- - **Style objects use CSS's own names, flat.** Objects in emotion and
74
- styled-components, StyleX, vanilla-extract, MUI's `sx`, Chakra's and
75
- Panda's style props, and React DOM's `style` all write
76
- `display: 'grid', gridTemplateColumns: '…'` and `gridColumn: 'span 2'`.
77
- None of them shortens anything inside the object. Yoga's own grid,
78
- unreleased, names its style `gridTemplateColumns` too, and CSS's masonry
79
- shipped as `display: grid-lanes`, reusing grid's properties.
80
- - **What is short lives a layer up**: Tailwind's `grid-cols-3` and
81
- `col-span-2`, or a component's props — Radix's `<Grid columns="3">`,
82
- Chakra's `<SimpleGrid columns={3}>`, Panda's `grid({ columns: 3 })`.
83
-
84
- So the style here is CSS's longhands, flat. A grid copied from a React DOM
85
- codebase lays out as written; a style array or a query block can change one
86
- property without restating the rest — `flattenStyle` replaces a `layout`
87
- object whole, so with the options bag a block that changed the columns had
88
- to restate the name and the areas too; and the day yoga's grid ships,
89
- handing it the work changes no style. The terse spelling belongs in the
90
- class-string layer the compact-styles prototype proposes, where Tailwind's
91
- names carry over as they are.
92
-
93
- **`display: 'grid'`, and `layout: 'grid'` beside it.** Every system
94
- above turns grid on with `display` — CSS, Tailwind's `grid` class, React
95
- DOM, yoga's `Display.Grid`, `display: grid-lanes` — so `display` does here
96
- too. `layout` stays the name every layout goes by, the Houdini role
97
- (`display: layout(name)` in CSS), and `layout: 'grid'` asks for the same
98
- grid. They disagree when `display: 'grid'` stands beside another layout, or
99
- `layout: 'grid'` beside `display: 'flex'`; the box is reported and laid out
100
- as flexbox until they agree (`layoutOf`, src/layouts.js). `display: 'flex'`
101
- beside `masonry` is no disagreement: it says the box is shown, which is all
102
- `display` said before grid was one of its values, and the docs' own
103
- `'@container …': { display: 'flex' }` to show a box again relies on it.
104
-
105
- A registered layout keeps `layout: { name, … }` and `layoutItem`: it cannot
106
- claim global style names. That is the asymmetry flexbox already has, whose
107
- properties are flat because they are CSS's.
108
-
109
- ### The values
110
-
111
- | on the box | CSS | |
112
- | ----------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------- |
113
- | `gridTemplateColumns`, `gridTemplateRows` | `grid-template-*` | px (logical), %, fr, `auto`, `min-content`, `max-content`, `minmax()`, `fit-content()`, `repeat()` |
114
- | `gridTemplateAreas` | `grid-template-areas` | one quoted string per row, or an array of the rows |
115
- | `gridAutoColumns`, `gridAutoRows` | `grid-auto-*` | the sizes an implicit track takes, in turn |
116
- | `gridAutoFlow` | `grid-auto-flow` | `row`, `column`, and `dense` |
117
- | `justifyItems` | `justify-items` | `stretch`, `flex-start`, `center`, `flex-end` |
118
- | **on a child** | | |
119
- | `gridColumn`, `gridRow` | `grid-column/row` | `2`, `'2 / 4'`, `'span 2'`, `'2 / span 3'`, `'1 / -1'` |
120
- | `gridArea` | `grid-area` | a name from `gridTemplateAreas`, or four lines |
121
- | `justifySelf` | `justify-self` | `auto`, and `justifyItems`' values |
122
-
123
- `gap`, `columnGap`, `rowGap`, `alignItems`, `alignSelf`, `justifyContent`
124
- and `alignContent` are the box's and the child's own flex properties, and
125
- mean for a grid what they mean in CSS.
126
-
127
- - **A number is a count in a template**: `gridTemplateColumns: 3` is
128
- `repeat(3, minmax(0, 1fr))`. Every layer that shortens grid means that by
129
- a number, and compiles it to exactly that string — Tailwind's
130
- `grid-cols-3`, Radix's `columns="3"`, Chakra's and Mantine's
131
- `SimpleGrid`, Panda's `grid()` pattern. A number that is not a length is
132
- no stranger here than `maxLines: 3` or `flex: 1`, and CSS gives a bare
133
- number no meaning in a track list. On `gridAutoRows` a number is a length
134
- — an implicit track has a size and never a count — and on `gridColumn` a
135
- line, as in CSS and in React DOM, which never gives it `px`.
136
- - **The alignment keywords are this renderer's**, the ones `alignItems`
137
- takes: `flex-start` and `flex-end` where CSS grid writes `start` and
138
- `end`. One spelling per concept; the error names the one to write.
139
- - **Checked where written.** The style validator runs the layout's own
140
- parsers in development (`validateValue`, src/styles.js), so a track list
141
- that would not lay out is an error naming the property and the fix — a
142
- unitless length says to write `px` and that a bare number is a count — not
143
- a layout that throws and falls back to flexbox.
144
-
145
- ### What the seam gained
146
-
147
- Two things, both general:
148
-
149
- - **`LayoutChild.style`**, the child's resolved style: what a grid places a
150
- child by and aligns it with. A registered layout can read a child's
151
- `alignSelf` the same way.
152
- - **`info.report(message, consequence)`**, to say a mistake the algorithm
153
- can lay out around — a `gridArea` naming no area, `layoutItem` on a grid's
154
- child — once, the way a bad style value is said. A throw stays for what
155
- cannot be laid out.
156
-
157
- And one thing the funnel had to learn: a grid's properties are not yoga's,
158
- so nothing dirtied a grid whose `gridTemplateColumns` changed.
159
- `_retarget` compares `GRID_CONTAINER_PROPS` and `GRID_ITEM_PROPS` and asks
160
- the host again, as it already did for `layoutItem`.
161
-
162
- ## 3. Performance
163
-
164
- `npm run bench:grid`: layout time per frame, `WindowNode._layoutStep`'s.
165
- The changes are an item's text going from two words to forty and back;
166
- changes and resizes are medians.
167
-
168
- | case | flexbox | grid | masonry |
169
- | ------------------------ | ------: | ------: | ------: |
170
- | form — mount | 36.4 ms | 28.6 ms | |
171
- | form — one text changed | 13.9 ms | 3.5 ms | |
172
- | form — resized 1000↔900 | 5.2 ms | 2.9 ms | |
173
- | form — live resize | 7.6 ms | 6.8 ms | |
174
- | cards — mount | 63.0 ms | 59.3 ms | 75.3 ms |
175
- | cards — one text changed | 13.1 ms | 2.1 ms | 1.8 ms |
176
- | cards — resized 1000↔900 | 20.2 ms | 5.2 ms | 4.3 ms |
177
- | cards — live resize | 25.8 ms | 20.3 ms | 27.9 ms |
178
- | dashboard — mount | | 15.2 ms | |
179
- | dashboard — one changed | | 2.2 ms | |
180
- | dashboard — live resize | | 4.5 ms | |
181
-
182
- The form is 200 rows of `auto 1fr` against flexbox rows with a fixed label
183
- width; the cards 300 cards of 9 yoga nodes each in
184
- `repeat(auto-fill, minmax(200px, 1fr))` against a flex-wrap row with a 200px
185
- basis; the dashboard 120 tiles in `repeat(6, 1fr)`, dense, a third two
186
- columns wide and a fifth two rows tall.
187
-
188
- **Two resizes, and the difference matters.** The study measured only the
189
- first: a window resized back and forth between two widths, which the seam
190
- answers from what each child said at those widths before. A live resize —
191
- a width nothing has been asked at, every frame, which is what a drag is —
192
- asks every child again; there grid is level with flexbox rather than four
193
- times cheaper. 801 child-tree layouts a frame for the 300 cards, under
194
- three a card.
195
-
196
- **Why it is cheaper than the prototype**, whose cards took 92 ms to mount
197
- and 36 ms a live-resize frame: a track asks its children for sizes only
198
- when its sizing function is intrinsic, as the spec has it. The gallery's
199
- `minmax(200px, 1fr)` has a fixed minimum, so its columns ask nothing — no
200
- max-content layout of any card — and each card is asked its height once,
201
- at the snapped width it is then laid out at, which is the question the
202
- placement's own check asks.
203
-
204
- **What the seam still costs**, found building this and left for changes of
205
- their own:
206
-
207
- - `probeHeightFloors` asks a host its height at its **border-box** width
208
- (`_heightForWidth`), where yoga's measure is given the content box: a
209
- grid with padding is asked at widths nothing is drawn at, each a layout
210
- of every child. Masonry's four layouts a card a live-resize frame are
211
- mostly that.
212
- - `intrinsicSizes()` lays a child out unconstrained for its max-content
213
- width even when only the minimum is read.
214
- - The floors pass asks a host its height at its narrowest, which lays
215
- every child out at a width no frame uses.
216
-
217
- ## 4. Held to Chrome
218
-
219
- `test/grid-conformance.test.js` lays out 400 grids that Chrome laid out
220
- first (`scripts/grid-fixture.mjs`, into `test/fixtures/grid-chrome.json`)
221
- and compares every item's rect, within a pixel. The grids are seeded and
222
- text-free — an item holds a box of a fixed size or a wrapping row of fixed
223
- chips — so both engines agree on every content size and the fixture is the
224
- same on any machine. Half the cases pile features together; the other half
225
- add exactly one of the harder ones — negative lines, a span wider than the
226
- grid, a line past it, self and content alignment, a definite height, column
227
- flow — to a case otherwise made of plain ones, so each feature has cases of
228
- its own.
229
-
230
- **399 of 400 match.** The one that does not is a case where Chrome and the
231
- spec part ways, and this grid keeps to the spec: an auto-placed item whose
232
- span in the flow's direction reaches several tracks past the explicit grid.
233
- The spec's cursor finds it room across the grid's edge; Chrome puts it
234
- after everything placed. Probed on its own: a straddle of one track lays
235
- out as Chrome does, in either flow; the cases that differ reach four or
236
- five tracks past it.
237
-
238
- Built against three more corpora of the same kind — 1,000 cases — it
239
- matched all but three, and those three are the one difference snapping the
240
- lines makes: Chrome lays a track out at 94.66 px and wraps a row of chips
241
- that fits in the 95 px here. The prototype, on the first two of those
242
- corpora, matched 97–99% of the cases using only what it claimed and 59–70%
243
- overall: negative lines threw, and neither self nor content alignment nor
244
- column flow was there.
245
-
246
- **The lines are snapped, not the items.** Tracks are sized exactly, and
247
- each track line is rounded to a whole pixel (`trackLines`); an item's rect
248
- runs from one rounded line to another. Rounding each item's own position
249
- and width, which the prototype did, left a seam of a pixel between
250
- neighbours in `1fr 1fr 1fr` and drew a 7px gap as 6. It would also lay a
251
- child's tree out at a fraction, the input yoga divides a rounding residue
252
- by (issue #411).
253
-
254
- `test/grid.test.js` holds the cases worth a name — and the renderer's
255
- side: the style, the disagreement between `display` and `layout`,
256
- validation, the reports, a grid following its style from frame to frame.
257
- Each of fifteen deliberate breaks of the implementation fails one of them.
258
-
259
- ## 5. What is left out
260
-
261
- - **Subgrid.** Impossible on this seam: a subgrid's items take part in the
262
- parent's track sizing, and an algorithm sees a child only as something to
263
- measure. The seam would have to hand an algorithm its grandchildren. A
264
- table whose rows share their columns is where it would pay.
265
- - **Baselines** (`alignItems: 'baseline'` in a row): yoga's baseline
266
- function, which the seam does not expose. Treated as `flex-start`.
267
- - **Named lines** (`[full-start] 1fr`): an error naming them; an area is
268
- the way to name a place.
269
- - **The `grid`, `grid-template` and `place-*` shorthands**: one spelling
270
- per concept, and the longhands are the ones every CSS-in-JS library
271
- writes.
272
- - **The second column pass** (§12.1 step 3): sizing the columns again when
273
- an item's min-content contribution changed once the rows were sized. It
274
- matters for items whose width follows their height.
275
- - **An auto repeat counted against a maximum width**: CSS counts
276
- `repeat(auto-fill, …)` against `max-width` when the width is indefinite;
277
- here it repeats once there.
278
- - **Percentage gaps**, and `order`.
279
-
280
- ## 6. Complexity
281
-
282
- `src/grid.js` is about 1,400 lines of code, 1,650 with its comments: the
283
- parsers, placement with implicit tracks on both sides, the track sizing
284
- algorithm with the spec's distribution of a spanning item's space —
285
- base sizes and growth limits in their separate steps, infinitely growable
286
- tracks, `fit-content()` caps — and alignment. For scale, Taffy's grid
287
- module is 7,508 lines (§8): the above, its corners and their tests.
288
-
289
- ## 7. Why not an engine
290
-
291
- - **Not yoga's.** Its grid landed in March 2026 as an API with no layout
292
- algorithm, the algorithm is in open PRs, and `yoga-layout` has not
293
- released on npm since December 2024. When it ships, a grid named with
294
- CSS's properties can hand it the work without a style changing.
295
- - **Not Taffy.** Its JS and WASM builds are third-party and 0.9–1.3 MB,
296
- against `yoga-layout`'s 224 KB, and it would be a second layout engine,
297
- with its own text-measuring bridge, for one algorithm.
298
-
299
- ## 8. Outside facts
300
-
301
- As of 2026-09-11.
302
-
303
- - **The spec.** [css-grid-2 §12](https://www.w3.org/TR/css-grid-2/#algo-grid-sizing),
304
- the grid sizing algorithm, and §8.5, placement.
305
- - **Prior art for the style.**
306
- - Tailwind v4: `grid-cols-<n>` compiles to `repeat(<n>, minmax(0, 1fr))`,
307
- `col-span-full` to `1 / -1`; arbitrary track lists are
308
- `grid-cols-[200px_1fr]`; there are no area utilities in core
309
- ([grid-template-columns](https://tailwindcss.com/docs/grid-template-columns),
310
- [grid-column](https://tailwindcss.com/docs/grid-column),
311
- [discussion #2634](https://github.com/tailwindlabs/tailwindcss/discussions/2634)).
312
- - Radix Themes' `<Grid columns="3">` compiles a count to
313
- `repeat(3, minmax(0, 1fr))`
314
- ([grid.props.tsx](https://github.com/radix-ui/themes/blob/HEAD/packages/radix-ui-themes/src/components/grid.props.tsx));
315
- so do Chakra's `SimpleGrid`
316
- ([simple-grid.tsx](https://github.com/chakra-ui/chakra-ui/blob/HEAD/packages/react/src/components/simple-grid/simple-grid.tsx)),
317
- Mantine's `SimpleGrid` and Panda's `grid()` pattern
318
- ([patterns](https://panda-css.com/docs/concepts/patterns)); Chakra's
319
- and Panda's own docs write `gridTemplateColumns` in full in a style
320
- object. Theme UI's `<Grid width={128}>` is
321
- `repeat(auto-fit, minmax(128px, 1fr))`
322
- ([Grid](https://theme-ui.com/components/grid)).
323
- - Yoga's grid, unreleased, names its style `gridTemplateColumns`
324
- (`yoga/style/Style.h` on react/yoga's main).
325
- - CSS's masonry shipped in Safari 26 as `display: grid-lanes`, reusing
326
- `grid-template-columns`, `gap` and `grid-column: span`
327
- ([WebKit](https://webkit.org/blog/17660/introducing-css-grid-lanes/)).
328
- - **Yoga.** Grid has been requested since 2019
329
- ([react/yoga#867](https://github.com/react/yoga/issues/867)). Part 1 of
330
- nine (#1893: style types, the C and JS API, `Display.Grid`) landed on
331
- 2026-03-05 with no layout algorithm
332
- ([07524851f54b](https://github.com/react/yoga/commit/07524851f54b)); the
333
- algorithm is in open PRs #1894, #1923 and
334
- [#1994](https://github.com/react/yoga/pull/1994). `yoga-layout` 3.2.1
335
- (2024-12-13) is still the latest release on
336
- [npm](https://www.npmjs.com/package/yoga-layout).
337
- - **Taffy.** `src/compute/grid` is 7,508 lines at
338
- [a1c3f8f](https://github.com/DioxusLabs/taffy/tree/a1c3f8fb3e2c978c3a4f062873bdb16d5eaf6cfd/src/compute/grid);
339
- subgrid ([#468](https://github.com/DioxusLabs/taffy/issues/468)) is an
340
- open issue. There is no official JS package
341
- ([#394](https://github.com/DioxusLabs/taffy/pull/394)); third-party
342
- builds are [`taffy-layout`](https://www.npmjs.com/package/taffy-layout),
343
- 902 KB unpacked, and
344
- [`@taffyjs/wasm`](https://www.npmjs.com/package/@taffyjs/wasm), 1.34 MB.
345
- - **Chromium.** LayoutNG caches a grid item's measure and layout passes
346
- ([LayoutNG](https://developer.chrome.com/docs/chromium/layoutng)); Firefox's
347
- nested grids cost exponentially in their depth until measurement caches
348
- fixed it ([1591366](https://bugzilla.mozilla.org/show_bug.cgi?id=1591366),
349
- [1682686](https://bugzilla.mozilla.org/show_bug.cgi?id=1682686)).
350
-
351
- ## 9. The scripts
352
-
353
- - `scripts/grid-fixture.mjs` regenerates the Chrome fixture (`CHROME=` to
354
- point it at a browser).
355
- - `scripts/bench/grid/bench.mjs` is the table in §3 (`npm run bench:grid`).
356
- - `scripts/bench/grid/breakdown.mjs` is one frame taken apart, run by run,
357
- with what the seam remembered for each child and what it had to ask
358
- again.