react-x11 2.51.1 → 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 (121) hide show
  1. package/README.md +94 -61
  2. package/docs/README.md +256 -0
  3. package/docs/accessibility.md +575 -0
  4. package/docs/appearance.md +317 -0
  5. package/docs/click-to-component.md +92 -0
  6. package/docs/clipboard.md +262 -0
  7. package/docs/components.md +1231 -0
  8. package/docs/dbus.md +205 -0
  9. package/docs/debugging.md +371 -0
  10. package/docs/desktop-calendar.md +247 -0
  11. package/docs/desktop.md +523 -0
  12. package/docs/devtools.md +133 -0
  13. package/docs/drag-and-drop.md +585 -0
  14. package/docs/ecosystem/animation.md +360 -0
  15. package/docs/ecosystem/data-fetching.md +186 -0
  16. package/docs/ecosystem/dev-tooling.md +393 -0
  17. package/docs/ecosystem/forms.md +277 -0
  18. package/docs/ecosystem/headless.md +194 -0
  19. package/docs/ecosystem/i18n.md +86 -0
  20. package/docs/ecosystem/icons.md +377 -0
  21. package/docs/ecosystem/layout.md +206 -0
  22. package/docs/ecosystem/routing.md +135 -0
  23. package/docs/ecosystem/state.md +344 -0
  24. package/docs/ecosystem/testing.md +474 -0
  25. package/docs/ecosystem/theming.md +494 -0
  26. package/docs/ecosystem.md +439 -0
  27. package/docs/elements.md +2079 -0
  28. package/docs/embedding.md +239 -0
  29. package/docs/events.md +742 -0
  30. package/docs/extending.md +1700 -0
  31. package/docs/eyedropper.md +164 -0
  32. package/docs/filedialog.md +222 -0
  33. package/docs/frame.md +436 -0
  34. package/docs/gl.md +173 -0
  35. package/docs/globalmenu.md +195 -0
  36. package/docs/macos.md +592 -0
  37. package/docs/notifications.md +150 -0
  38. package/docs/packaging.md +468 -0
  39. package/docs/permissions.md +143 -0
  40. package/docs/react-features.md +405 -0
  41. package/docs/remote.md +179 -0
  42. package/docs/scale.md +225 -0
  43. package/docs/security.md +132 -0
  44. package/docs/styling.md +1710 -0
  45. package/docs/system.md +519 -0
  46. package/docs/testing.md +494 -0
  47. package/docs/typescript.md +75 -0
  48. package/docs/uri-schemes.md +340 -0
  49. package/docs/wayland-backend.md +332 -0
  50. package/docs/windows-integrations.md +177 -0
  51. package/docs/windows.md +380 -0
  52. package/package.json +11 -3
  53. package/src/Reconciler.js +2 -2
  54. package/src/a11y.js +1 -1
  55. package/src/backend/context2d.js +3 -3
  56. package/src/bootstrap.js +1 -1
  57. package/src/bus.js +39 -26
  58. package/src/cli/create.d.ts +43 -0
  59. package/src/cli/create.js +305 -0
  60. package/src/cli/env.js +484 -0
  61. package/src/cli/index.js +29 -2
  62. package/src/cli/templates/AGENTS.template.md +195 -0
  63. package/src/cli/templates/DESIGN.template.md +269 -0
  64. package/src/cocoa/app.js +6 -6
  65. package/src/cocoa/bezels.js +1 -1
  66. package/src/cocoa/fonts.js +1 -1
  67. package/src/cocoa/globalmenu.js +1 -1
  68. package/src/cocoa/keymap.js +1 -1
  69. package/src/cocoa/panewindow.js +1 -1
  70. package/src/cocoa/player.js +1 -1
  71. package/src/cocoa/presenter.js +2 -2
  72. package/src/cocoa/promotion.js +1 -1
  73. package/src/cocoa/relaunch.js +1 -1
  74. package/src/cocoa/threaded.js +1 -1
  75. package/src/cocoa/video.js +1 -1
  76. package/src/cocoa/window.js +4 -4
  77. package/src/components/Menu.js +7 -2
  78. package/src/components/Switch.js +1 -1
  79. package/src/components/native.js +1 -1
  80. package/src/desktopintegration.js +2 -1
  81. package/src/desktopsettings.js +193 -24
  82. package/src/dnd.js +2 -2
  83. package/src/filedialog.js +3 -3
  84. package/src/glnodes.js +1 -1
  85. package/src/grid.js +1 -1
  86. package/src/keyboardstate.js +28 -0
  87. package/src/layouts.js +1 -1
  88. package/src/node.d.ts +10 -10
  89. package/src/nodes/animation.js +1 -1
  90. package/src/nodes/invalidate.js +1 -1
  91. package/src/nodes/layouthost.js +1 -1
  92. package/src/nodes/video.js +19 -8
  93. package/src/nodes/window/droptarget.js +1 -1
  94. package/src/pacing.js +1 -1
  95. package/src/portal.js +31 -0
  96. package/src/refresh/index.js +2 -1
  97. package/src/screencolor.js +5 -10
  98. package/src/types/system.d.ts +8 -6
  99. package/src/types/video.d.ts +5 -2
  100. package/src/videobackend.js +28 -0
  101. package/src/videoframes.js +29 -16
  102. package/src/wayland/app.js +86 -0
  103. package/src/wayland/clipboard.js +14 -7
  104. package/src/wayland/connection.js +3 -0
  105. package/src/wayland/context2d.js +129 -10
  106. package/src/wayland/protocols/index.json +6 -0
  107. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  108. package/src/wayland/seat.js +28 -1
  109. package/src/wayland/swapchain.js +23 -0
  110. package/src/wayland/video.js +376 -0
  111. package/src/wayland/xkb.js +137 -0
  112. package/src/win32/a11y.js +1 -1
  113. package/src/win32/app.js +2 -2
  114. package/src/win32/fonts.js +1 -1
  115. package/src/win32/glarea.js +4 -4
  116. package/src/win32/overlay.js +1 -1
  117. package/src/win32/panewindow.js +3 -3
  118. package/src/win32/shell.js +1 -1
  119. package/src/win32/window.js +2 -2
  120. package/src/windowid.js +1 -1
  121. package/src/xvideo.js +189 -0
@@ -0,0 +1,1710 @@
1
+ # Styling
2
+
3
+ Style lives in one place: the `style` prop. A style property passed flat is
4
+ an error in development that names the fix.
5
+
6
+ ## The rule
7
+
8
+ One namespace per kind of thing, and no name in both:
9
+
10
+ - **`style`** — everything CSS has a concept for: layout, paint, text,
11
+ `cursor`, `overflow`, `zIndex`, `pointerEvents`, `container`.
12
+ - **props** — everything else: `title`, window geometry and size hints,
13
+ `resizable`, `appId`, `windowType`, `grab`, `value`, `src`, `focusable`,
14
+ `tabIndex`, `disabled`, handlers. The 3D elements of
15
+ `@react-x11/components/three`, under a reconciler of their own, keep flat
16
+ property props (`position`, `material`) — those are object properties, not
17
+ CSS.
18
+
19
+ `style` takes an object or a nested array, flattened left-to-right with
20
+ falsy entries skipped. That replaces the **selector** cascade: precedence is
21
+ written at the call site instead of resolved by specificity.
22
+
23
+ ```jsx
24
+ <box style={[s.card, isWide && s.wide, { backgroundColor: theme.panel }]} />
25
+ ```
26
+
27
+ Nothing matches a rule against the tree, but inheritance does happen: a
28
+ handful of properties travel down it — see
29
+ [Inheritance](#inheritance-the-ink-the-face-and-the-size).
30
+
31
+ ## Lengths and shorthands
32
+
33
+ A length is a distance in **logical pixels**: `12` and `'12px'` are the same
34
+ value, and the display scale turns either into device pixels (see
35
+ [scale.md](scale.md)). Where the layout takes them, `'50%'` and `'auto'`
36
+ mean what they mean in CSS; a border's or an outline's width also takes
37
+ `'thin'`, `'medium'` and `'thick'` (1, 3 and 5).
38
+
39
+ `null`, like `undefined`, is unset: `width: wide ? 240 : null` leaves the
40
+ width to the layout, and `''` is an unset colour. A value a property cannot
41
+ take (`width: 'hidden'`, `padding: 'auto'`, `flexDirection: 'sideways'`) or
42
+ a colour that does not parse (`'nonsense'`, `7`, `'#12'`) is an error at the
43
+ commit in development, naming the property — in a state block, a gradient
44
+ stop, a shadow, and a query block that does not match yet too. One that
45
+ gets past that — a `$token` resolving to it, or a production build — is
46
+ dropped where it is applied and reported, the way CSS drops a declaration it
47
+ cannot parse: the rest of the block still applies, and a dropped colour
48
+ paints no fill, stroke, gradient or shadow at all rather than half of one,
49
+ and the inherited ink for text.
50
+
51
+ ```jsx
52
+ <box
53
+ style={{
54
+ padding: '8px 16px',
55
+ margin: '0 auto',
56
+ border: '1px solid $border',
57
+ borderBottom: '2px solid $accent',
58
+ background: 'linear-gradient($surface, $background) $surface',
59
+ ':focus-visible': { outline: '2px solid $accent' },
60
+ }}
61
+ />
62
+ ```
63
+
64
+ CSS's shorthands are read the way CSS reads them, and expand into the
65
+ longhands before anything else sees the style:
66
+
67
+ | shorthand | sets |
68
+ | ------------------------------------------------- | ----------------------------------------------------------------------------------------- |
69
+ | `padding`, `margin` | the four sides, by CSS's one-to-four rule — `'8px 16px'`, `'0 auto'`, `'1 2 3'` |
70
+ | `inset` | `top`, `right`, `bottom`, `left`, the same way — `inset: 0` |
71
+ | `borderWidth`, `borderColor` | the four sides' widths or colours, the same way |
72
+ | `paddingInline`, `marginInline` | `…Start` and `…End`: one value for both, or two |
73
+ | `paddingBlock`, `marginBlock` | `…Top` and `…Bottom`, the same way |
74
+ | `gap` | `rowGap` and `columnGap` — `'4px 8px'` is the row gap first, as in CSS |
75
+ | `border` | `borderWidth`, `borderStyle`, `borderColor`: `'1px solid $border'` |
76
+ | `borderTop`/`Right`/`Bottom`/`Left`/`Start`/`End` | that side's width and colour: `borderBottom: '1px solid $border'` |
77
+ | `outline` | `outlineWidth` and `outlineColor`; `'none'` is `outlineWidth: 0` |
78
+ | `background` | `backgroundColor` and `backgroundImage` — a colour, a gradient, or both |
79
+ | `flex` | `flexGrow`, `flexShrink`, `flexBasis` — `1`, `'auto'`, `'none'`, `'1 1 0'`, `'0 0 240px'` |
80
+
81
+ A `$token` may stand for any one value inside them — `padding: '$gutter 0'`,
82
+ `border: '$hairline solid $border'` — and resolves before the value is read.
83
+
84
+ **The more specific property wins, wherever it is written.** A longhand
85
+ beats its shorthand, a side beats all four sides, and a pair beats all four
86
+ too: `paddingTop` beside `padding: '8px 16px'` is the top padding whether it
87
+ comes before or after it in the style. CSS goes by order instead; a
88
+ flattened style array has no order left to go by.
89
+
90
+ A part a shorthand leaves out is left unset rather than reset to CSS's
91
+ initial value, since a longhand written beside it would win anyway. Three
92
+ things differ from CSS on purpose, and say so:
93
+
94
+ - **A line with no width is `medium`, 3px**, as in CSS; but a line with no
95
+ style is `'solid'` rather than CSS's `none`, because `borderWidth` alone
96
+ has always drawn here. `'none'` is a width of 0 — `border: 'none'`,
97
+ `outline: 'none'`.
98
+ - **A line with no colour is [`currentColor`](#currentcolor)**, the node's
99
+ ink, as in CSS — but the weakest thing in the style rather than whatever
100
+ came last: a colour the style names for that side anywhere else is the one
101
+ it takes, so `borderBottom: '1px solid'` beside `borderColor: '$border'`
102
+ is a `$border` line wherever each is written.
103
+ - **A box has one `borderStyle`.** `border`'s style is the box's, and a
104
+ side's only speaks when `border` named none; two shorthands naming
105
+ different ones are an error unless `borderStyle` says which.
106
+
107
+ What is not here says what to write instead: `font` (the longhands),
108
+ `paddingHorizontal` (React Native's name — `paddingInline`), a per-corner
109
+ radius (`borderRadius` rounds all four), `border-radius` (style names are
110
+ camelCase), and a misspelt name gets the nearest real ones. `background` and
111
+ `outline` are paint all the way down, so a state block may set them;
112
+ `border` sets a width, which is layout, so it may not. A `transition` keyed
113
+ by a shorthand reaches the longhands it expands into —
114
+ `transition: { border: 150 }` fades the `borderColor` a `border` set.
115
+
116
+ ### Relative units
117
+
118
+ Every length also takes CSS's relative units, resolved for the node it is
119
+ written on into the same logical pixels:
120
+
121
+ | unit | one of it is | resolved again when |
122
+ | -------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
123
+ | `em` | the node's own font size — and on `fontSize` itself, the size it inherits, so `'1.5em'` is half again its parent's | an ancestor's font size changes |
124
+ | `rem` | the palette's `fontSize`: the size every label that names none is set in, 14 by default | the theme changes |
125
+ | `vw`, `vh`, `vmin`, `vmax` | a hundredth of the window the node is in — the `<window>` or `<popup>`, the size a `'@width >= 600'` block asks about | the window is resized |
126
+
127
+ `rem` is read from the nearest palette, the way a `$token` is: a
128
+ `<ThemeProvider value={{ fontSize: 12 }}>` around a dense panel re-bases it,
129
+ and an app porting web numbers that assume 16 sets `fontSize: 16` on its
130
+ theme.
131
+
132
+ A **percentage on `fontSize`** is of the inherited size, as `em` is. A
133
+ **percentage `borderRadius`** is of the box's shorter side: a corner here is
134
+ a circle, so `'50%'` is a circle on a square and a pill on anything longer,
135
+ where CSS would draw each corner as an ellipse. Everywhere else a percentage
136
+ is the layout's, as before.
137
+
138
+ `calc()`, `min()`, `max()` and `clamp()` over any of these resolve to one
139
+ number where the style is — `'calc(1rem + 2px)'`, and fluid type as
140
+ `fontSize: 'clamp(14px, 1rem + 0.5vw, 20px)'`. A **percentage takes part
141
+ only alone** (`'calc(100% / 3)'`): yoga takes one unit per value, and what a
142
+ percentage is of is decided inside the layout, so `'calc(100% - 16px)'` is an
143
+ error that says so. Pad a `'100%'` box rather than subtracting from it, or
144
+ give the room to `flexGrow`.
145
+
146
+ `lineHeight` takes all of CSS's forms and comes to the multiple the text
147
+ engines take: `1.5`, `'150%'` and `'1.5em'` are that multiple, and a length
148
+ — `'24px'`, `'1.75rem'` — is divided by the node's own font size, which is
149
+ what makes a design's "16 on 24" the line it asks for.
150
+
151
+ A relative length is re-resolved only on the nodes that wrote one, when what
152
+ it reads changes; a style written in pixels costs nothing. Inside an
153
+ `'auto'`-sized window a viewport unit makes the content depend on the window
154
+ that depends on the content; like a size query, it settles where the two
155
+ agree, so size such a window or keep the viewport units out of it.
156
+
157
+ Some of CSS's units are **not read**, and a value in one is an error naming
158
+ the unit and why — `invalid width "2ex" in <box style>: ex is not read — it
159
+ needs the font's x-height`:
160
+
161
+ - `ex` and `ch`, which need font metrics nothing measures yet (`0.5em` is the
162
+ usual stand-in);
163
+ - the container units, `cqw` and the rest: a container's size is known only
164
+ after a layout pass, and a length in it would lay the tree out again on
165
+ every frame the container moves — a percentage of the parent, or an
166
+ `'@container'` block, says the same without that;
167
+ - the print units, `pt`, `mm`, `in`: lengths here are logical pixels, which
168
+ the display scale turns into device ones.
169
+
170
+ So is a second value where a property takes one.
171
+
172
+ ## What it fixes on `<window>`
173
+
174
+ `width`/`height` on a `<window>` are the real window's geometry — the user
175
+ can drag them — so they are props and never reach the layout, and the window
176
+ manager's size hints sit beside them, flat:
177
+
178
+ ```jsx
179
+ <window
180
+ title="Editor"
181
+ width={900}
182
+ height={600}
183
+ x={40}
184
+ y={40} // the X window
185
+ minWidth={400}
186
+ minHeight={300}
187
+ resizable // WM hints, flat, no nesting
188
+ style={{ flexDirection: 'row', backgroundColor: '#1e1e1e' }} // the root box
189
+ />
190
+ ```
191
+
192
+ `style` is the root box, and its `backgroundColor` is also the window's own
193
+ background colour.
194
+
195
+ ## Inline pseudo-states
196
+
197
+ The thing inline CSS cannot do, and the reason people keep a stylesheet:
198
+
199
+ ```jsx
200
+ <box
201
+ style={{
202
+ backgroundColor: theme.surface,
203
+ ':hover': { backgroundColor: theme.surfaceHover },
204
+ ':focus': { borderColor: theme.borderFocus },
205
+ }}
206
+ />
207
+ ```
208
+
209
+ `:hover`, `:focus-within`, `:focus`, `:focus-visible`, `:active`,
210
+ `:disabled`, `:drag-over`, `:dragging`. These are **node states, not
211
+ selectors**: each is something the node itself already knows, so there is no
212
+ specificity and no matching. A state change recomputes one node's style and
213
+ repaints — **no React render.**
214
+
215
+ Precedence is fixed and low-to-high: `:hover` → `:focus-within` → `:focus` →
216
+ `:focus-visible` → `:active` → `:disabled` → `:drag-over` → `:dragging`,
217
+ merged per property, so a disabled control never looks hovered, a node that
218
+ is itself focused can say something narrower than one that merely contains
219
+ focus, and a drag in progress outranks all of the pointer and focus states.
220
+ Because the hover _path_ is the ancestor chain, hovering a child lights up an
221
+ ancestor's `:hover` block, exactly like CSS.
222
+
223
+ `:active` is a press, and follows the same chain, so the control draws the
224
+ press whichever of its parts — a label, a thumb — is under the pointer. It
225
+ is **live for the whole gesture**: it drops when the pointer leaves the
226
+ chain and comes back when it returns, so it always means "releasing now
227
+ activates this". A control acts on the release, so give every interactive
228
+ thing an `:active` look: it is the only thing it can show while it is held
229
+ ([components.md](components.md#the-press-state)).
230
+
231
+ ```jsx
232
+ <box
233
+ focusable
234
+ style={{
235
+ backgroundColor: theme.background,
236
+ ':hover': { backgroundColor: theme.surfaceHover },
237
+ ':active': { backgroundColor: theme.surfaceActive },
238
+ }}
239
+ />
240
+ ```
241
+
242
+ A `transition` on the same property is welcome — what matters is that the
243
+ change _starts_ on the press frame, not that it finishes there.
244
+
245
+ The keyboard has no press state: Space and Enter act on the down, so the
246
+ activation is immediate, but nothing draws held-ness — on X11 a held key
247
+ cannot be told from fast tapping.
248
+
249
+ `:focus-visible` is focus that came from the keyboard — Tab, an arrow inside
250
+ a widget, `autoFocus`, `node.focus()`, a modal handing focus back as it
251
+ closes. A **press** sets `:focus` and not `:focus-visible`, as in CSS: the
252
+ user knows where they clicked, and a ring on every click is noise, where a
253
+ ring on Tab is the only cue a keyboard user has. Put focus rings in
254
+ `:focus-visible` and colour changes that are welcome either way in `:focus`.
255
+
256
+ The exception is CSS's too: a press on a **text control** — `<textinput>`,
257
+ `<textarea>`, or a custom element reporting editable text — does set
258
+ `:focus-visible`, because what the click told the user is where the caret
259
+ went, not that every keystroke from now on lands there.
260
+
261
+ `:focus-within` is focus on this node **or inside it** — CSS's, and the
262
+ answer to "the row should light up while the field in it is being typed
263
+ into". A `<popup>` counts as inside the node it hangs off in the JSX tree,
264
+ so a `Select` with its menu open still reads as focused.
265
+
266
+ ```jsx
267
+ <box
268
+ style={{
269
+ borderColor: theme.border,
270
+ ':focus-within': { borderColor: theme.borderFocus },
271
+ }}
272
+ >
273
+ <textinput value={value} onChange={setValue} />
274
+ </box>
275
+ ```
276
+
277
+ The last two belong to drag and drop. `:drag-over` follows the pointer
278
+ during a drag on exactly the same ancestor-path rule as `:hover` — and,
279
+ like `:hover`, it says where the pointer is, not whether the node would
280
+ accept the drop; `useDropTarget`'s `isAccepted` is the one that answers
281
+ that. `:dragging` is set on the source node for the duration of a drag.
282
+ See [drag-and-drop.md](drag-and-drop.md).
283
+
284
+ **State blocks may only set paint properties** (`backgroundColor`,
285
+ `backgroundImage`, `borderColor` and the per-side `borderTopColor`/…,
286
+ `borderRadius`, `boxShadow`, `zIndex`, `outlineWidth`, `outlineColor`,
287
+ `outlineOffset`, `opacity`, `color`, and the `background` and `outline`
288
+ shorthands) — enforced at declaration time by `createStyles`, so that a
289
+ pointer move never reflows the tree. Anything that changes layout or what
290
+ renders stays in React state.
291
+
292
+ There are **no relational selectors** — no `:hover > child`, no sibling
293
+ combinator, no Tailwind-style `group`. A parent's `:hover` reaches its
294
+ children because `color` is
295
+ [inherited](#inheritance-the-ink-the-face-and-the-size), a child's hover
296
+ reaches its ancestors because the hover path is the ancestor chain, and a
297
+ sibling reacting to a sibling stays in React ([In components](#in-components)).
298
+
299
+ ### The focus ring
300
+
301
+ **Every focusable node draws one already**, on `:focus-visible`, with no
302
+ styling at all — a bare `<box focusable>` included. It is not something an
303
+ application opts into, because a keyboard user cannot opt into needing it.
304
+ A text field gets it from a click as well as from Tab; everything else waits
305
+ for the keyboard (see `:focus-visible` above).
306
+
307
+ `outlineWidth`, `outlineColor` and `outlineOffset` override it, and they are
308
+ paint properties like any other: animatable, legal in a state block, and
309
+ painted **outside the border box** so switching one on cannot move the thing
310
+ it surrounds.
311
+
312
+ ```jsx
313
+ <box focusable /> {/* ring, for free */}
314
+ <box focusable style={{ outlineWidth: 0 }} /> {/* opted out */}
315
+ <box
316
+ focusable
317
+ style={{ ':focus-visible': { outlineWidth: 3, outlineColor: '#e17055' } }}
318
+ />
319
+ ```
320
+
321
+ A theme sets `focusRing`, `focusRingWidth` and `focusRingOffset` to restyle
322
+ every ring under it at once — the renderer reads them from the nearest
323
+ `theme` prop, so `<ThemeProvider>` covers the widgets and anything an
324
+ application writes itself.
325
+
326
+ ## Direction, and the logical edges
327
+
328
+ ```jsx
329
+ <box style={{ direction: 'rtl' }}>
330
+ <box style={{ flexDirection: 'row', paddingStart: 12, gap: 8 }}>…</box>
331
+ </box>
332
+ ```
333
+
334
+ `direction` is CSS's, `'ltr' | 'rtl' | 'inherit'`, and it inherits: setting
335
+ it mirrors that subtree and nothing above it. Rows run the other way, `flex-start`
336
+ is the right-hand end, and every logical edge below swaps sides with it.
337
+
338
+ **The default comes from the locale.** An app started under
339
+ `LANG=ar_EG.UTF-8` is mirrored with no configuration, as GTK and Qt apps
340
+ are. Every locale that is not written right to left answers `'ltr'`, so an
341
+ app that never thinks about this pays nothing. The palette carries it —
342
+ `theme.direction` — so an app with a language menu switches the whole UI with
343
+ the `<ThemeProvider>` swap it was already doing for colours:
344
+
345
+ ```jsx
346
+ <ThemeProvider value={{ direction: settings.rtl ? 'rtl' : 'ltr' }}>
347
+ ```
348
+
349
+ Prefer the provider over a bare `direction` style when the region contains
350
+ **widgets**. Yoga mirrors boxes on its own, so most of the widget set needs
351
+ nothing — a `<Checkbox>` is a row with a gap — but the decisions yoga cannot
352
+ make (which way an arrow key steps, which way a chevron points, which side a
353
+ submenu opens on) are read from the palette through `useDirection()`. A
354
+ provider plants the matching style property in the tree as it goes, so both
355
+ routes always agree under one.
356
+
357
+ ### The logical edges
358
+
359
+ | logical | physical in LTR | in RTL |
360
+ | -------------------------------- | ------------------------------ | ------- |
361
+ | `start` / `end` | `left` / `right` | swapped |
362
+ | `marginStart` / `marginEnd` | `marginLeft` / `marginRight` | swapped |
363
+ | `paddingStart` / `paddingEnd` | `paddingLeft` / `paddingRight` | swapped |
364
+ | `borderStartWidth` / `…EndWidth` | `borderLeftWidth` / `…Right…` | swapped |
365
+ | `borderStartColor` / `…EndColor` | `borderLeftColor` / `…Right…` | swapped |
366
+
367
+ **A logical edge wins over the physical one even in LTR**, the way CSS's
368
+ `padding-inline-start` wins over `padding-left`, and the physical side wins
369
+ over all four sides.
370
+
371
+ Write layout in the logical pair and it is the same layout in both
372
+ directions. Use the physical one when you mean the screen: a drop shadow, a
373
+ resize handle in a fixed corner.
374
+
375
+ ### What does not mirror
376
+
377
+ - **`<canvas>`** — the drawing is the application's, and `ctx` keeps its
378
+ top-left origin. An app that wants mirrored art reads `node.direction`
379
+ from the ref it already has.
380
+ - **A `<window>`'s or `<popup>`'s explicit `x`/`y`** — screen coordinates.
381
+ - **Vertical anything.** `top`/`bottom`, a column's order, a vertical
382
+ scrollbar's travel, Up/Down on every control.
383
+ - **The caret keys in a field.** Left and Right step through the _string_,
384
+ not across the screen, and so do Home and End — the editing model is
385
+ written in logical positions.
386
+
387
+ What **does**, besides the box tree: a vertical scrollbar moves to the left;
388
+ horizontal scrolling counts from the right-hand edge, so `scrollX: 0` still
389
+ means "at the beginning"; `textAlign: 'start'`/`'end'` resolve against the
390
+ box's direction rather than against the first strong character; a popup
391
+ prefers the start side, so a submenu opens leftwards and still flips at the
392
+ screen edge; and the inside of a `<textinput>`/`<textarea>` — see below.
393
+
394
+ ### Inside an editable field
395
+
396
+ A field is laid out at its box's direction, exactly as a `<text>` is, and the
397
+ direction does two separate things there:
398
+
399
+ - **It is the base level the value is shaped at.** A run of neutral
400
+ characters — `"(1) 12:30"`, a filename, a lone bracket — resolves against
401
+ it rather than against the first strong character, so an Arabic word typed
402
+ into an English form in an **LTR** window does not reorder the line as
403
+ though it were Arabic.
404
+ - **It is the edge the text is against.** The value, the placeholder and the
405
+ caret start at the direction's start edge — the right-hand one under
406
+ `direction: 'rtl'` — and an unscrolled field still shows the _beginning_ of
407
+ its value, so a value too long for its box overflows towards the left.
408
+ Clicking, the caret, the selection bands and the scroll mirror together.
409
+
410
+ `textAlign` works on a field too: `'start'` (the default) is the base
411
+ direction's start edge, and `'center'`/`'end'` do what they say while the
412
+ value fits. A value that overflows its box is pinned to the start edge
413
+ whatever the alignment says, because that is what it is scrolled from.
414
+
415
+ The text's _own_ direction is not consulted: a Latin value in an RTL field is
416
+ placed at the field's right edge, the way `<input dir="rtl">` places one, and
417
+ there is no `dir="auto"` equivalent.
418
+
419
+ ## Per-side borders
420
+
421
+ ```jsx
422
+ <box
423
+ style={{ borderLeftWidth: 3, borderLeftColor: '$border', paddingLeft: 10 }}
424
+ >
425
+ {children} {/* a blockquote bar, no extra node */}
426
+ </box>
427
+ ```
428
+
429
+ A side width overrides `borderWidth` exactly the way `paddingLeft`
430
+ overrides `padding`, `borderBottom: '1px solid $border'` sets one side's
431
+ width and colour together, and a side colour
432
+ (`borderTopColor`/`Right`/`Bottom`/`Left`) falls back to `borderColor`.
433
+ `borderStartWidth`/`borderStartColor` and their `End` counterparts are the
434
+ [logical](#the-logical-edges) pair, and they win over the physical side. The
435
+ widths are layout — yoga sees each edge, so the bar above insets content on
436
+ the left only — and the colours are paint, legal in a state block like
437
+ `borderColor` itself.
438
+
439
+ Build a rule or an accent edge this way — a blockquote's bar, a table's row
440
+ separators (`borderBottomWidth: 1` on each row), a tab's underline — rather
441
+ than out of 1px `<box>`es, which pay a node per row in layout and paint.
442
+
443
+ Two limits:
444
+
445
+ - **`borderRadius` requires uniform borders** — same width and colour on
446
+ all four sides. A non-uniform border paints square, ignores the radius,
447
+ and says so once in development.
448
+ - Corners between two painted sides of different colours are square and
449
+ deterministic: top and bottom span the box's full width, left and right
450
+ run between them, where CSS mitres the corner diagonally.
451
+
452
+ ## Gradients and shadows
453
+
454
+ ```jsx
455
+ <box
456
+ style={{
457
+ padding: 16,
458
+ borderRadius: 10,
459
+ backgroundImage: 'linear-gradient(135deg, $accent, $accentActive)',
460
+ boxShadow: '0 2px 8px rgba(0, 0, 0, .4)',
461
+ ':hover': { boxShadow: '0 6px 20px rgba(0, 0, 0, .5)' },
462
+ }}
463
+ >
464
+ <text style={{ color: '$accentText' }}>Header</text>
465
+ </box>
466
+ ```
467
+
468
+ Both are **paint** properties: legal in a state block, and outside layout
469
+ entirely — a gradient fills the box the layout already decided on, and a
470
+ shadow is drawn beyond it and moves nothing. Both are written in CSS's
471
+ spelling, and a `$token` resolves **inside** them as well as as a whole
472
+ value, which is what keeps a themed decoration in a hoisted style.
473
+
474
+ ### `backgroundImage`
475
+
476
+ `linear-gradient(<direction>?, <stop>, <stop>, …)` or `'none'`, resolved
477
+ against the node's own box so it works wherever the node lands.
478
+
479
+ - The direction is an angle in degrees clockwise from "up" (`135deg`), a side
480
+ (`to right`), or a corner (`to bottom right`). Left out, it is `to bottom`.
481
+ - A stop is a colour, optionally followed by a `%` or a pixel position.
482
+ Positions left out are spread evenly; one that goes backwards is pulled up
483
+ to the one before it, so two stops at the same offset are a hard break.
484
+ - It paints **over** `backgroundColor`, which is CSS's order — a translucent
485
+ gradient tints the colour underneath it rather than replacing it.
486
+
487
+ Only linear gradients exist. A radial or conic one is a `<canvas onDraw>`
488
+ away (`createRadialGradient` draws on every backend but Windows', and
489
+ ntk's context has `createConicalGradient`).
490
+
491
+ On X11 a **square** gradient is as cheap as a colour, but a **rounded** one
492
+ is rasterized on the client and uploaded per fill — about 4 KB per box per
493
+ repaint, paid only where something changed. A list of rounded gradient rows
494
+ is the one shape to think twice about.
495
+
496
+ ### `boxShadow`
497
+
498
+ `<x> <y> [blur] [spread] [colour]`, comma-separated for several, painted
499
+ first-on-top like CSS's. The colour may be left out, which means the node's
500
+ own `color` — [`currentColor`](#currentcolor), which may be written out
501
+ too. A blurred shadow is a real gaussian, computed once per distinct size,
502
+ radius and blur and cached, so a list of identical cards renders one and
503
+ composites it many times, and a repaint costs the same however wide the
504
+ blur. On macOS and Windows it is the 2D context's own `shadowBlur`.
505
+
506
+ What it does not do, and why:
507
+
508
+ - **`inset` throws.** An inner shadow is a different drawing, and painting an
509
+ outer one where an inner was asked for is a bug with no visible cause. An
510
+ inset border or a `<canvas>` says it today.
511
+ - **Ignored on `<window>` and `<popup>`** (with a warning in development). A
512
+ shadow is painted outside the box, and a toplevel owns no pixels there —
513
+ a real one needs the window to carry a translucent margin of its own.
514
+ Put the shadow on a `<box>` inside the window; for a floating menu, the
515
+ popup's own `borderRadius` plus a border is what reads as raised.
516
+ - **Neither transitions.** Both are several numbers and a colour in one
517
+ string; they snap. A card that wants to rise on hover animates the
518
+ `backgroundColor` or the border beside them.
519
+
520
+ A shadow inks pixels the node does not own, so it widens what the node
521
+ repaints — its offset, its spread and the blur's tail. Prefer one shadow on
522
+ the card to one on every row inside it.
523
+
524
+ ## Opacity
525
+
526
+ `opacity` fades a node **and everything in it, as one**, the way CSS's does:
527
+
528
+ ```jsx
529
+ <box style={{ opacity: enabled ? 1 : 0.5, transition: { opacity: 150 } }}>
530
+ <svg viewBox="0 0 16 16">…</svg>
531
+ <text>Reminders</text>
532
+ </box>
533
+ ```
534
+
535
+ That is the difference from a colour's own alpha. `rgba(…, .5)` on a card's
536
+ background fades the background; it cannot fade the card, because every part
537
+ of it — the border, the icon, the caption — is drawn over the others, and at
538
+ half alpha each one shows what is under it: the card through the icon. With
539
+ `opacity` the subtree is drawn once, into an offscreen surface the size of
540
+ what it paints, and the surface is composited at the alpha.
541
+
542
+ It is a **paint** property: legal in a state block, and it transitions and
543
+ loops like the others — a halo that pulses, a section that fades in:
544
+
545
+ ```jsx
546
+ animation: { opacity: { from: 1, to: 0.5, duration: 800, alternate: true } }
547
+ ```
548
+
549
+ What it costs is that surface: one per faded node, the size of its paint
550
+ bounds inside the window, kept from one frame to the next while the node stays
551
+ faded and released when it is opaque again. `0` draws nothing and costs
552
+ nothing — and the node is still laid out and still hit, as in CSS. A value
553
+ outside 0 to 1 is clamped; one that is not a number throws in development.
554
+
555
+ ## Measuring text to its letters
556
+
557
+ ```jsx
558
+ <text style={{ fontSize: 24, textBoxTrim: 'cap-alphabetic', padding: 12 }}>
559
+ HEX
560
+ </text>
561
+ ```
562
+
563
+ `textBoxTrim` is CSS's `text-box-trim: trim-both` with `text-box-edge: cap
564
+ alphabetic`: the `<text>`'s box becomes the capitals down to the last
565
+ baseline, so padding around a label is measured from the letters and
566
+ centring centres what you can see. Default `'none'`.
567
+
568
+ **A line box is not the text**: the space over a capital differs from the
569
+ space under a baseline by `(ascent - capHeight) - descent`, a property of
570
+ the typeface, so an untrimmed label is only optically centred by luck.
571
+ **`lineHeight` is not an alternative**: it is a multiplier over the natural
572
+ line box, and the leading still splits evenly above and below, so it changes
573
+ how much space there is, never how it is balanced.
574
+
575
+ Two things worth knowing:
576
+
577
+ - It applies to `<text>`. `<textinput>` and `<textarea>` keep their full line
578
+ box, because their caret and selection geometry is measured against it.
579
+ - Trimming removes real space, so a control gets shorter: size the padding
580
+ for the result you want rather than to whatever the metrics happened to
581
+ add.
582
+
583
+ **The built-in widgets set it on every label**, which is why `paddingY` in a
584
+ palette is bigger than a CSS padding for the same look — it is the space you
585
+ see, not the space plus whatever the ascent left over. Which way an untrimmed
586
+ label is off changes with the face (it rides low in SF NS and high in
587
+ Helvetica), and a widget does not know the face it will be drawn in.
588
+
589
+ A glyph drawn as text — a check mark, a submenu arrow, an icon — is centred
590
+ on its own middle rather than sitting on a baseline, so trimming its box
591
+ moves it off centre. Those keep the full line box, and so should yours.
592
+
593
+ `<textinput>` cannot trim — its caret and selection are measured against the
594
+ full line box — so it reaches the same place from the other side: its **box**
595
+ is the cap band, its baseline goes where the space above the capitals equals
596
+ the space under it, and the glyphs are allowed to hang out of the box the way
597
+ a trimmed label's descenders do. The drawing is clipped one step out, at the
598
+ padding box, so an ascender and a descender are both there and neither can
599
+ reach the border. The caret and the selection follow, being drawn from the
600
+ same origin.
601
+
602
+ That is what makes a field the same height as the controls beside it. Padding
603
+ it with the palette's `paddingY` gives exactly the height a `<Button>` and a
604
+ `<Select>` have, because all three are now the same sum — the capitals, plus
605
+ that padding twice, plus the border:
606
+
607
+ ```jsx
608
+ <textinput
609
+ style={{
610
+ paddingTop: '$paddingY',
611
+ paddingBottom: '$paddingY',
612
+ paddingLeft: 10,
613
+ paddingRight: 10,
614
+ borderWidth: '$borderWidth',
615
+ }}
616
+ />
617
+ ```
618
+
619
+ `<textarea>` keeps its full line boxes, box and clip both: it is line spacing
620
+ that a paragraph is made of, and nothing hangs out of a stack of them.
621
+
622
+ ## Keeping text on one line, and saying when it did not fit
623
+
624
+ ```jsx
625
+ <text style={{ textWrap: 'nowrap' }}>{row.modified}</text>
626
+ ```
627
+
628
+ `textWrap: 'nowrap'` is CSS's, and it is what separates a cell from a
629
+ paragraph. A `<text>` measures height-for-width: hand it a narrow box and it
630
+ wraps to fit, which is right for prose and wrong for a row of a fixed height —
631
+ a date that wraps is sliced off top and bottom and spills over the rows
632
+ either side. `'nowrap'` measures at unbounded width, so the overflow is
633
+ horizontal, which `overflow: 'hidden'` on the box around it clips. Default
634
+ `'wrap'`.
635
+
636
+ A date always fits, so clipping it would be a bug rather than a design. A
637
+ **name** does not: clipped, a truncated file name and a short one look
638
+ equally complete.
639
+
640
+ ```jsx
641
+ <text style={{ textWrap: 'nowrap', textOverflow: 'ellipsis' }}>
642
+ {file.name}
643
+ </text>
644
+ ```
645
+
646
+ `textOverflow` is CSS's `text-overflow`: `'clip'` (the default) slices
647
+ mid-glyph, `'ellipsis'` ends the line in a `…`. It is the careful version, not
648
+ a substring —
649
+
650
+ - the `…` is set in the font of the run it cut into, so an elided line ending
651
+ in a large or bold word gets a matching mark rather than one in the
652
+ paragraph's base style, and it falls back to `...` where neither that font
653
+ nor any fallback covers U+2026;
654
+ - the cut is at a grapheme boundary and the tail is **re-shaped**, because
655
+ kerning and ligatures across the cut change widths;
656
+ - it cuts the **visually** last run rather than the logically last one, so a
657
+ right-to-left line ends on its left.
658
+
659
+ `maxLines` is the other half — CSS's `-webkit-line-clamp`. A three-line blurb
660
+ that ends in a `…` is `{ maxLines: 3, textOverflow: 'ellipsis' }`; the lines
661
+ past the cap are dropped before the box is measured, so the height is the
662
+ kept lines' and not the whole paragraph's.
663
+
664
+ **`textOverflow: 'ellipsis'` on its own means one line.** Eliding happens off
665
+ a line count — the mark stands for the lines that were dropped — so an
666
+ ellipsis with no cap would have nothing to say and would silently do nothing.
667
+ One line is what a name, a path or a status line wants; `maxLines` is how to
668
+ ask for two or three.
669
+
670
+ ### What `nowrap` and `ellipsis` do together
671
+
672
+ They change what the `<text>` reports to layout, which is worth knowing
673
+ before a row moves under you.
674
+
675
+ A clipping `nowrap` label is measured at unbounded width, so it tells its
676
+ container it needs the **whole string** — and CSS's automatic minimum size
677
+ turns that into a floor. It cannot be squeezed, so a long name pushes the
678
+ column next to it out of the row and the overflow is dealt with somewhere
679
+ above.
680
+
681
+ An eliding one is measured against the width on offer, so its floor is small
682
+ and it **gives way**: it takes the room that is left, shows `Applicati…`,
683
+ and the column beside it keeps its width. That is the behaviour a table
684
+ wants, and `<Table>` sets `nowrap` and `ellipsis` on every cell and header
685
+ for that reason.
686
+
687
+ What is truncated is a fact about the pixels and nothing else. A `<text>` that
688
+ was elided still reports the **whole** string as its accessible name, and the
689
+ caret and selection indices still index into the whole string.
690
+
691
+ ## Inheritance: the ink, the face and the size
692
+
693
+ Ten properties travel down the tree, and they are the ones CSS calls
694
+ inherited:
695
+
696
+ | property | |
697
+ | ----------------------- | ------------------------------------------ |
698
+ | `color` | the ink |
699
+ | `fontFamily` | the face |
700
+ | `fontSize` | the size |
701
+ | `fontWeight` | |
702
+ | `fontStyle` | |
703
+ | `fontVariationSettings` | a variable font's remaining axes |
704
+ | `textRendering` | how glyph origins are rounded at draw time |
705
+ | `letterSpacing` | the space after every character |
706
+ | `fontVariantNumeric` | which figures: tabular, oldstyle… |
707
+ | `fontFeatureSettings` | any other OpenType feature, by tag |
708
+
709
+ So a block of quiet type is a `<box>` and not a decision repeated at every
710
+ label inside it:
711
+
712
+ ```jsx
713
+ <box style={{ color: theme.textMuted, fontSize: 12 }}>
714
+ <text>Last modified</text>
715
+ <text>{row.modified}</text>
716
+ <Icon name="moreVertical" />
717
+ </box>
718
+ ```
719
+
720
+ A style property on the node itself still wins, the way a property always
721
+ wins over what a node inherits — and under the outermost element is the
722
+ palette, so text that names none of this is set in the theme's `text`,
723
+ `fontFamily` and `fontSize` ([theme tokens](#theme-tokens) below).
724
+
725
+ It reaches everything that draws with type, not just `<text>`: a
726
+ `<textinput>`, a `<canvas mono>` (which is what an `<Icon>` is), an `<svg>`
727
+ resolving `fill="currentColor"`, and any custom element
728
+ that asks `node.resolvedTextStyle()`
729
+ ([extending.md](extending.md#text-of-your-own)). A nested `<text>` span is
730
+ the same mechanism seen from closer up.
731
+
732
+ **A `:hover` block that sets `color` therefore reaches the labels inside.**
733
+ That is how CSS behaves and it is why there is no "group hover" here to
734
+ learn: `:hover` marks the row, `color` is inherited, and the row's label and
735
+ its icon follow.
736
+
737
+ ```jsx
738
+ <box style={{ color: theme.text, ':hover': { color: theme.accent } }}>
739
+ <text>Open recent</text>
740
+ <Icon name="chevronRight" />
741
+ </box>
742
+ ```
743
+
744
+ Nothing about that costs a layout pass: a state block sets paint and `color`
745
+ only, so the only inherited property a pointer can move is the ink, which
746
+ repaints without re-measuring. A `fontSize` change _does_ re-measure, and it
747
+ can only come from a React commit, a size query or a theme.
748
+
749
+ What does **not** inherit: `textAlign`, `lineHeight`, `textWrap`,
750
+ `textOverflow`, `maxLines` and `textBoxTrim`. CSS inherits the first two;
751
+ here they are read by the node that owns the **box** the text flows in, and a
752
+ box is not something a descendant has. `<Icon>`'s `size` does not inherit either — a glyph is a
753
+ drawing rather than a letter, so it takes its default from the palette's
754
+ `fontSize` and stays put when a label around it shrinks.
755
+
756
+ **A value that is not one inherits.** `fontSize: '1.2ex'`, `fontStyle: 7`,
757
+ `fontWeight: 1200` or `lineHeight: 'tall'` is an error at the commit in
758
+ development, naming the property; where one gets past that — a `$token`, or
759
+ a production build — the text sets as though it had not been written, the way
760
+ CSS inherits past a declaration it cannot parse. Numbers may be numeric
761
+ strings (`fontSize: '20'`), and a size is a
762
+ [length](#lengths-and-shorthands) like any other: `fontSize: '20px'` is 20,
763
+ and `'1.5em'` or `'150%'` is half again the size inherited. A `lineHeight`
764
+ in a length — `'24px'` — is that multiple of the node's own size.
765
+
766
+ ### `currentColor`
767
+
768
+ The ink as a colour: `currentColor` is the node's own `color`, or the one it
769
+ inherits. It goes wherever a colour does — `backgroundColor`, `borderColor`
770
+ and each side of it, `outlineColor`, a `boxShadow`'s colour, a gradient stop,
771
+ and inside `border`, `outline` and `background` — and a line those
772
+ shorthands give no colour is drawn in it, as in CSS:
773
+
774
+ ```jsx
775
+ <box style={{ color: theme.textMuted, ':hover': { color: theme.accent } }}>
776
+ <box style={{ borderBottom: '2px solid' }}>
777
+ <text>Inbox</text>
778
+ </box>
779
+ </box>
780
+ ```
781
+
782
+ The label and its underline turn together, and nothing re-renders. It is
783
+ CSS's keyword in any case CSS takes it (`currentcolor` reads too), and on
784
+ `color` itself it is the colour inherited — CSS's `inherit`.
785
+
786
+ It stays the keyword in the node's style, which is CSS's computed value, and
787
+ becomes a colour where it is painted, so it follows the ink wherever the ink
788
+ moves: a `:hover` on a row above, a theme swap, a `color` transition frame by
789
+ frame. A node painting in it repaints when its ink moves, bounded to that
790
+ node. A `<popup>` written under the row inherits the ink like any other
791
+ child and repaints with it.
792
+
793
+ Two places it cannot simply follow:
794
+
795
+ - **A transition runs between the colours at the swap.** `borderColor`
796
+ going from `'#d0d0d0'` to `'currentColor'` under a `transition` fades to
797
+ the ink as it is when the style changes, and lands on the keyword, which
798
+ follows the ink from then on. An ink moving under a `currentColor` that
799
+ stayed is not a change of the property and does not transition: the border
800
+ is the new ink at once. To fade it, transition `color`, and the border
801
+ rides along.
802
+ - **A loop cannot end in it.** A loop's ends are fixed for as long as it
803
+ runs, so `animation: { borderColor: { to: 'currentColor', … } }` is an
804
+ error saying so. Loop `color` instead, and write the border in
805
+ `currentColor`.
806
+
807
+ An element of your own that paints a colour from its style asks
808
+ `node.resolveColor(value)` for it
809
+ ([extending.md](extending.md#text-of-your-own)).
810
+
811
+ ## A font file of your own
812
+
813
+ An app that ships a face, or that shows one — a picker, a specimen, a
814
+ preferences page with a family name in it — asks for the file by path (or by
815
+ bytes) and gets back what to draw it with:
816
+
817
+ ```jsx
818
+ import { loadFont } from 'react-x11';
819
+
820
+ // `app` is the ntk connection — the one `createRoot({ app })` took, or
821
+ // `useApp()`'s inside a tree
822
+ const { font, family } = loadFont(app, '/path/to/Inter.ttf');
823
+
824
+ <text style={{ fontFamily: family, fontSize: 20 }}>Handgloves</text>;
825
+ ```
826
+
827
+ `family` comes **off the file** rather than out of the caller's head: it is
828
+ the font's own name, so an app that ships `Inter.ttf` goes on writing
829
+ `fontFamily: 'Inter'` in the styles it already has, and a registered face
830
+ beats fontconfig for that name — which is the point of shipping one. Several
831
+ faces of one family — regular, bold, italic — all keep the name and
832
+ `fontWeight` picks between them. Only a second file that would be
833
+ _unreachable_ under it, the same family at the same weight and slant, is
834
+ scoped: that one comes back as `Inter 2`. Draw with the name that comes back
835
+ and the question never arises.
836
+
837
+ `loadFont(app, path, { family: 'preview' })` names it yourself instead —
838
+ worth doing for a file the _user_ picked, to keep it out of the way of the
839
+ app's own type. `weight` and `style` override what the file claims about
840
+ itself, and `postscriptName` picks one face out of a `.ttc`.
841
+
842
+ From a component, `useFont` is the same thing with the connection already in
843
+ hand, and it returns null before anything is picked:
844
+
845
+ ```jsx
846
+ function Specimen({ path }) {
847
+ const picked = useFont(path); // path may be null
848
+ return <text style={{ fontFamily: picked?.family }}>Handgloves</text>;
849
+ }
850
+ ```
851
+
852
+ A file that will not parse throws, which an error boundary is the right home
853
+ for when the app ships the font and the wrong one when a user just chose it
854
+ in a dialog — reach for `loadFont` in that handler and catch there.
855
+
856
+ ### Reading a font without installing it
857
+
858
+ `openFont(app, source)` reads the file and changes nothing else:
859
+
860
+ ```js
861
+ const font = openFont(app, path);
862
+ font.metrics(30); // what the renderer lays out with
863
+ font.variationAxes; // { wght: { name, min, default, max } }
864
+ font.hasGlyph(0x20b8); // is the tenge sign in this face?
865
+ ```
866
+
867
+ The difference from `loadFont` is worth knowing before a font browser picks
868
+ one over the other. Registering a face does not only make a `fontFamily`
869
+ resolve to it: every registered font is consulted, ahead of the system
870
+ fonts, for **any codepoint the current face is missing**. That is exactly
871
+ what an app shipping a symbol or icon face wants, and exactly what an app
872
+ that is merely _previewing_ files does not — it would quietly change which
873
+ face draws the bullets and the curly quotes in its own UI.
874
+
875
+ Both verbs read a given file once per connection, however often they are
876
+ called: `useFont` in a component that re-renders sixty times a second parses
877
+ nothing, and `loadFont` after `openFont` on the same path registers the face
878
+ already open.
879
+
880
+ `font` is ntk's `Font`, which has more on it than the four members above
881
+ ([ntk's docs/fonts.md](https://github.com/sidorares/ntk/blob/master/docs/fonts.md)). If you need
882
+ anything else of ntk's — `Path2D`, `Image`, `Surface` — **import it from
883
+ `react-x11/ntk`, never from `ntk`**, and never declare `ntk` as a dependency
884
+ of your own. Two copies in one process are two font caches and two glyph
885
+ atlases, and a node built against one copy cannot be painted by the other.
886
+
887
+ ## `createStyles`
888
+
889
+ A hoisted style object keeps its identity, so `applyProps` skips the whole
890
+ update with a `===` check. `createStyles` also validates keys, which a bare
891
+ object literal cannot: an unknown style property is an error at declaration
892
+ instead of a silent no-op.
893
+
894
+ ## `flattenStyle`
895
+
896
+ `flattenStyle(style)` collapses whatever the `style` prop accepts — an
897
+ object, an array, nested arrays, holes — into one plain object, which is what
898
+ the reconciler itself does before applying props. Two details are worth
899
+ knowing if you call it: a lone object is returned **as-is**, not copied, so
900
+ `===` still identifies a hoisted style; and a state block merges with one
901
+ already collected rather than replacing it, so
902
+ `[{ ':hover': { color } }, { ':hover': { backgroundColor } }]` keeps both.
903
+
904
+ ## In components
905
+
906
+ Every component takes `style` and merges it after its own, so an override
907
+ wins by position instead of clobbering a computed value:
908
+ `style: [control.style, base, checked && on, style]`.
909
+
910
+ `useControl(disabled, onActivate, { styled: true })` stops holding hover,
911
+ focus and the press in React state: no enter/leave handlers, no re-render on
912
+ pointer move. `Switch` and `Button` are the worked examples. Without it the
913
+ hook returns `hover`/`focused`/`pressed` as React state, which is what a
914
+ control needs when the part that has to change is not on the press chain —
915
+ a `Checkbox`'s well is a sibling of the label the press lands on, and no
916
+ node-local state block can cross that.
917
+
918
+ A component's own props are never style. `ProgressBar` takes `color`,
919
+ `Dialog` takes `width`/`height` — a dialog is a real popup window and needs
920
+ its geometry up front — and `ContextMenu` takes `fontSize` because it
921
+ measures labels with it. A `Select`'s or a `Slider`'s width is
922
+ `style={{ width }}`.
923
+
924
+ ## Theme tokens
925
+
926
+ A style value of `'$name'` resolves against the nearest theme above the node
927
+ — `<ThemeProvider value={palette}>`, or a `theme` prop on any element. The
928
+ sigil is what keeps it unambiguous: `'red'` is a CSS colour, `'$red'` is a
929
+ token.
930
+
931
+ ```jsx
932
+ const s = createStyles({
933
+ card: { backgroundColor: '$panel', padding: '$gutter' },
934
+ title: { color: '$text', fontSize: 20 },
935
+ });
936
+
937
+ <window theme={palette} style={{ backgroundColor: '$bg' }}>
938
+ <box style={s.card}>…</box>
939
+ </window>;
940
+ ```
941
+
942
+ A **hoisted** style — declared once, outside render, with no access to React
943
+ context — still follows the theme, with no palette threaded to every element
944
+ that paints.
945
+
946
+ Tokens are not colour-only; `padding: '$gutter'` resolves a number just as
947
+ well. And they resolve wherever a style value is written: in a state block, in
948
+ an `animation` declaration, and in a size, container or capability query block
949
+ — `'@width >= 600': { padding: '$gutter' }` is the wide layout's gutter from
950
+ the same palette.
951
+
952
+ Three of them are read with no `$` anywhere, because they are what text falls
953
+ back to rather than something a style asked for: **`text`, `fontFamily` and
954
+ `fontSize` are the ink, the face and the size of every `<text>` that names
955
+ none of its own** ([components.md](components.md#theming)). They are the floor
956
+ under [inheritance](#inheritance-the-ink-the-face-and-the-size) — what a node
957
+ resolves to when no element above it named one either — which is what makes
958
+ `<ThemeProvider value={{ fontFamily: 'Inter' }}>` a sentence an app says once.
959
+ A style property still wins over it, the way a style property always wins over
960
+ what a node inherits:
961
+
962
+ ```jsx
963
+ <window theme={{ fontFamily: 'Inter', fontSize: 16 }}>
964
+ <text>Inter at 16</text>
965
+ <text style={{ fontFamily: '$monoFamily' }}>the palette's mono face, 16</text>
966
+ <text style={{ fontSize: 24 }}>Inter at 24</text>
967
+ </window>
968
+ ```
969
+
970
+ **The floor is read from the window, not from the nearest provider.** That is
971
+ the sentence to remember, because the failing shape looks right: a
972
+ `<ThemeProvider value={palette}>` _inside_ a `<window>` feeds `useTheme()` and
973
+ resolves every `$token` under it, so the styles that name a face get one — and
974
+ every `<text>` that names none still falls back to the **window's** palette,
975
+ which is the desktop's `sans-serif` unless the window itself was given a
976
+ theme. An app that ships its own fonts then draws its headings in the face it
977
+ shipped and its body copy in whatever the machine calls sans-serif, which is
978
+ both wrong and slow: resolving a generic family costs an `fc-match` per weight
979
+ on the first paint (~110ms each).
980
+
981
+ Three ways to say it. For a whole app the first says it once, for the widgets
982
+ and every window; the last is the one for a subtree:
983
+
984
+ ```jsx
985
+ <ThemeProvider value={palette}> {/* above the windows: they take it */}
986
+ <App />
987
+ </ThemeProvider>
988
+
989
+ <window theme={palette}> {/* the floor, for everything inside */}
990
+
991
+ <box style={{ fontFamily: '$fontFamily', color: '$text' }}>
992
+ {/* or hand the face to a subtree by inheritance, which needs no window */}
993
+ </box>
994
+ ```
995
+
996
+ `monoFamily` has no such fallback — nothing is monospace unless it says so,
997
+ with `fontFamily: '$monoFamily'`, so an app's code surfaces are set from one
998
+ place.
999
+
1000
+ A `theme` prop anywhere scopes its subtree, and an inner one merges over the
1001
+ outer, so a panel can restate a colour or two without repeating a palette.
1002
+ Popups resolve through their place in the **tree**, not their window, so a
1003
+ menu inherits the theme of the UI that opened it even though it is a
1004
+ separate X window.
1005
+
1006
+ Note that a raw `theme` prop is exactly the object you wrote: it merges, but
1007
+ nothing is computed from it. The widget palette's derived tokens — the
1008
+ pressed `accentActive`/`surfaceActive`/`textMutedActive`/`dangerActive`, the
1009
+ `…Text` ink on each fill, and `surface` following `background` — are filled in
1010
+ by `<ThemeProvider>`, which plants the resolved palette in the tree for this
1011
+ lookup to find. Hand-writing `theme={{ accentHover: … }}` on a box gets the
1012
+ merge and not the derivation.
1013
+
1014
+ Changing the theme restyles the subtree in place, nodes whose own props did
1015
+ not change included.
1016
+
1017
+ An unknown token is **reported and dropped**: the message names the token,
1018
+ the element, the component that wrote it, and every token the palette in
1019
+ force does have; that one property goes missing, so the widget paints
1020
+ visibly wrong rather than the app dying; and `process.exitCode` is set to 1,
1021
+ so a test run, a CI job or a supervisor still counts the run as failed. It
1022
+ is not a throw because resolution also happens where no error boundary can
1023
+ catch one — above every boundary inside a `<window>`, or from the desktop's
1024
+ light/dark switch with no React on the stack.
1025
+
1026
+ Set **`REACT_X11_STRICT_TOKENS=1`** to make it fatal — for a kiosk, or
1027
+ a build that would rather stop than paint something wrong. Strict mode
1028
+ throws from the offending node's own `commitMount`, so an error boundary at
1029
+ any depth inside the window catches it and shows a fallback for that
1030
+ subtree. Where React genuinely is not on the stack — the desktop appearance
1031
+ switch — strict mode crashes, which is what it was asked for.
1032
+
1033
+ A token with **no** theme above it at all warns in dev — that one is
1034
+ otherwise silent, since the whole style is stripped rather than one value
1035
+ failing. Resolution is cached per (style object, theme object), so a hoisted
1036
+ style under one theme keeps its identity across renders and the `===` fast
1037
+ path still applies; a misspelling in a shared style is reported for every
1038
+ node that wears it, not just the first.
1039
+
1040
+ Resolution walks the node tree, not React context, so a palette has to reach
1041
+ the tree to be seen. `<ThemeProvider>` puts it in both places — the context
1042
+ widgets read with `useTheme()`, and a `theme` prop on a node
1043
+ ([components.md](components.md#theming)). Widgets plant their own merged
1044
+ palette on their root node as well, so `$tokens` work inside a widget
1045
+ subtree — and in a style you pass one — with no provider anywhere.
1046
+
1047
+ With no `theme` prop above it at all, a token resolves against **the
1048
+ desktop's palette** — `backgroundColor: '$background'` in an app that never
1049
+ wrote a `<ThemeProvider>` is dark on a dark desktop
1050
+ ([appearance.md](appearance.md)).
1051
+
1052
+ ## Sticky positioning
1053
+
1054
+ `position: 'sticky'` is CSS's: the box is laid out in flow, exactly where
1055
+ `relative` would put it, and then held inside the nearest scroll pane by its
1056
+ insets. `top: 0` keeps a section's header at the top of the pane while the
1057
+ section scrolls under it — until the section's end arrives and carries the
1058
+ header off, which is how the next section's header takes over.
1059
+
1060
+ ```jsx
1061
+ const s = createStyles({
1062
+ pane: { overflow: 'scroll', flexGrow: 1 },
1063
+ section: { flexShrink: 0 },
1064
+ header: { position: 'sticky', top: 0, backgroundColor: '$background' },
1065
+ });
1066
+
1067
+ <box style={s.pane}>
1068
+ {groups.map((group) => (
1069
+ <box key={group.letter} style={s.section}>
1070
+ <text style={s.header}>{group.letter}</text>
1071
+ {group.people.map((person) => (
1072
+ <Row key={person.id} person={person} />
1073
+ ))}
1074
+ </box>
1075
+ ))}
1076
+ </box>;
1077
+ ```
1078
+
1079
+ - **The insets are thresholds, not offsets.** `top: 12` says the box's top
1080
+ edge may come no nearer than 12px to the pane's top edge; it does not move
1081
+ a box that is already further down. `top`, `bottom`, `left`, `right`,
1082
+ `start` and `end` all work, in any combination, and a percentage is of the
1083
+ pane's size. An edge with no inset does not stick.
1084
+ - **The pane is the nearest ancestor with `overflow: 'scroll'`** — a `<box>`
1085
+ or the `<window>`. Its edges are inside its border and over its padding:
1086
+ the band the content scrolls through. `overflow: 'hidden'` clips without
1087
+ scrolling, so — unlike on the web — a rounded card between a header and
1088
+ the pane does not quietly become the pane the header sticks to. With
1089
+ nothing above that scrolls, the box stays where layout put it.
1090
+ - **The parent is the limit.** The box's margin box never leaves its
1091
+ parent's content box, and that is the push. Wrap each header with the rows
1092
+ it heads, as above: a header that is a sibling of every row has the whole
1093
+ list for a parent and sticks for the whole list. A direct child of the
1094
+ pane has the pane's content for a parent, and sticks for the whole scroll.
1095
+ - **It paints over its siblings.** A sticky box paints after its siblings of
1096
+ the same `zIndex`, as CSS paints a positioned box over the ones in flow, so
1097
+ the rows that scroll under a header do not paint over it, and it takes the
1098
+ press where it covers them. `zIndex` still orders it first. Give it a
1099
+ background, or the rows show through.
1100
+ - **It lands in the frame that scrolled**, with no re-render, so a header
1101
+ cannot trail its content by a frame; a header riding with its rows costs
1102
+ nothing, and a held one repaints its own rect.
1103
+ - **`onLayout` reports where layout put the box**, not where it is held,
1104
+ the same way a scroll does not fire it. Where it is held is
1105
+ `getClientRects()`.
1106
+
1107
+ Both axes at once, and one sticky box inside another, are what
1108
+ `examples/schedule.jsx` is made of: day banners pushed off by the next day, a
1109
+ row of room names held under the banner, a column of times held at the left,
1110
+ and the corner where the two meet.
1111
+
1112
+ Not supported: CSS's `scroll-padding`. Scrolling a node into view — keyboard
1113
+ focus reaching a row — brings it to the pane's edge, which can be under a
1114
+ held header.
1115
+
1116
+ Sticky is the built-in [custom position](#custom-positions): the same
1117
+ placement, written against the same context a registered one is handed.
1118
+
1119
+ ## Custom positions
1120
+
1121
+ A position is CSS's positioning scheme: `relative` lays the box out in flow
1122
+ and moves it by its insets, `sticky` lays it out in flow and holds it against
1123
+ a scroll pane's edges. `registerPosition` adds one of your own — the box laid
1124
+ out in flow, then moved by an offset your scheme works out once the layout
1125
+ pass is done, in the same frame:
1126
+
1127
+ ```jsx
1128
+ import { registerPosition } from 'react-x11/host';
1129
+
1130
+ // a layer that scrolls at a share of the pane's speed
1131
+ registerPosition('parallax', {
1132
+ options: { rate: { type: 'number', default: 0.5 } },
1133
+ place: (node, { pane, options }) =>
1134
+ pane ? { x: 0, y: Math.round(pane.scrollY * options.rate) } : null,
1135
+ });
1136
+
1137
+ <box style={{ overflow: 'scroll', flexGrow: 1 }}>
1138
+ <image src={hero} style={{ height: 240, position: 'parallax' }} />
1139
+ <box style={{ position: { name: 'parallax', rate: 0.2 } }}>…</box>
1140
+ </box>;
1141
+ ```
1142
+
1143
+ - **It is a `position` value**, by name or with its options, and switches
1144
+ like one — from a container query block too. The box is laid out exactly
1145
+ where `relative` would put it, and moved; nothing around it reflows. A box
1146
+ has one `position`, so to combine a scheme with `absolute`, wrap it in a
1147
+ box.
1148
+ - **The insets are the scheme's.** Yoga never sees them as offsets: sticky
1149
+ reads them as thresholds, and a registered position reads them as whatever
1150
+ it documents.
1151
+ - **It runs on every layout pass**, the one a scroll runs included, so what
1152
+ it moves lands in the frame that scrolled; one that animates asks for the
1153
+ next frame itself. It paints and takes the press over its siblings of the
1154
+ same `zIndex`, like sticky.
1155
+ - **Options are typed**: a `length` is written in logical pixels like any
1156
+ length in a style, and a wrong option is reported, naming it, and takes
1157
+ its default.
1158
+
1159
+ The contract — what `place` is handed and may return — is in
1160
+ [extending.md](extending.md#a-position-of-your-own).
1161
+
1162
+ ## Grid
1163
+
1164
+ `display: 'grid'` lays a box's children out as a CSS grid, written the way
1165
+ CSS writes one — `gridTemplateColumns` on the box, `gridColumn` on a child —
1166
+ so a grid copied from a web codebase lays out here as it was written:
1167
+
1168
+ ```jsx
1169
+ // a form: the labels as wide as the widest, the fields taking the rest
1170
+ <box
1171
+ style={{
1172
+ display: 'grid',
1173
+ gridTemplateColumns: 'auto 1fr',
1174
+ columnGap: 12,
1175
+ rowGap: 6,
1176
+ }}
1177
+ >
1178
+ <text>Name</text>
1179
+ <textinput />
1180
+ <text>Postal address</text>
1181
+ <textarea />
1182
+ <box style={{ gridColumn: '1 / -1' }}>{/* a row the whole width */}</box>
1183
+ </box>
1184
+
1185
+ // a gallery: as many columns of 200 and up as fit, each row as tall as its
1186
+ // tallest card
1187
+ <box
1188
+ style={{
1189
+ display: 'grid',
1190
+ gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
1191
+ gap: 8,
1192
+ }}
1193
+ >
1194
+ {cards}
1195
+ </box>
1196
+
1197
+ // an app's shell, by named areas
1198
+ <box
1199
+ style={{
1200
+ display: 'grid',
1201
+ gridTemplateColumns: '200px 1fr',
1202
+ gridTemplateRows: 'auto 1fr auto',
1203
+ gridTemplateAreas: ['head head', 'side main', 'foot foot'],
1204
+ flexGrow: 1,
1205
+ }}
1206
+ >
1207
+ <Header style={{ gridArea: 'head' }} />
1208
+ <Sidebar style={{ gridArea: 'side' }} />
1209
+ <Main style={{ gridArea: 'main' }} />
1210
+ <Footer style={{ gridArea: 'foot' }} />
1211
+ </box>
1212
+ ```
1213
+
1214
+ | on the grid box | |
1215
+ | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
1216
+ | `gridTemplateColumns`, `gridTemplateRows` | a track list, or a number: that many equal tracks |
1217
+ | `gridTemplateAreas` | one quoted string per row — `'"head head" "side main"'` — or an array of the rows |
1218
+ | `gridAutoColumns`, `gridAutoRows` | the size of a track the grid adds for a child placed past its template; a number is a length |
1219
+ | `gridAutoFlow` | `'row'` (the default) or `'column'`, and `'dense'` to back-fill a hole |
1220
+ | `gap`, `columnGap`, `rowGap` | between the tracks |
1221
+ | `justifyItems`, `alignItems` | where each child sits in its area: `'stretch'` (the default), `'flex-start'`, `'center'`, `'flex-end'` |
1222
+ | `justifyContent`, `alignContent` | where the tracks sit when they leave room |
1223
+ | **on a child** | |
1224
+ | `gridColumn`, `gridRow` | `2`, `'span 2'`, `'2 / 4'`, `'2 / span 3'`, `'1 / -1'` |
1225
+ | `gridArea` | a name from `gridTemplateAreas`, or four lines; `gridColumn` and `gridRow` beside it win for their own axis |
1226
+ | `justifySelf`, `alignSelf` | its own place in its area, over the grid's |
1227
+
1228
+ Each property is CSS's of the same name (`gridTemplateColumns` is
1229
+ `grid-template-columns`). A track list takes lengths in `px` (logical
1230
+ pixels), `%` and `fr`; `auto`, `min-content`, `max-content`; `minmax()`,
1231
+ `fit-content()`; and `repeat()` with a count, `auto-fill` or `auto-fit`.
1232
+
1233
+ Things to know:
1234
+
1235
+ - **A number is a count** where CSS's value is a track list:
1236
+ `gridTemplateColumns: 3` is three equal columns, `repeat(3, minmax(0,
1237
+ 1fr))`, the way Tailwind's `grid-cols-3` means it — equal whatever is in
1238
+ them. `'repeat(3, 1fr)'` is CSS's own, a column never narrower than what is
1239
+ in it, so one long word widens its column. Inside a track list a length
1240
+ takes its unit, `'200px 1fr'`; on `gridAutoRows` a number is a length, and
1241
+ on `gridColumn` a line, counting from 1 at the start and from -1 at the
1242
+ end.
1243
+ - **`layout: 'grid'` is the same request**, under the name every layout goes
1244
+ by ([below](#custom-layouts)). A box whose `display` and `layout` disagree
1245
+ — `display: 'grid'` beside `layout: 'masonry'`, or `layout: 'grid'` beside
1246
+ `display: 'flex'` — is reported, and laid out as flexbox until they agree.
1247
+ - **The alignment keywords are this renderer's**, the ones `alignItems`
1248
+ already takes: `'flex-start'` and `'flex-end'` where CSS grid writes
1249
+ `start` and `end`. A value it does not take is an error listing the ones
1250
+ it does.
1251
+ - **The lines are whole pixels.** Tracks are sized exactly and their lines
1252
+ snapped, so neighbours in `'1fr 1fr 1fr'` meet on a shared pixel and every
1253
+ gap is as wide as it was written. Chrome lays a track out at a fraction of
1254
+ a pixel, so a row whose content fits in the fraction can wrap one line
1255
+ differently here.
1256
+ - **It matches Chrome** within a pixel, but for the fraction above.
1257
+ - **Not here:** subgrid (a grid's child lays its own children out), baseline
1258
+ alignment, named lines (`[full-start]` — name an area instead) and the
1259
+ `grid` and `grid-template` shorthands. A value that uses one is an error
1260
+ naming it.
1261
+ - **It runs inside the layout pass**, as every layout does — on a window's
1262
+ resize, a live one included, and in the frame a container query flips. A
1263
+ query block can switch one property alone:
1264
+ `'@container width < 500': { gridTemplateColumns: 1 }`. In a scroll pane,
1265
+ the grid is a `<box>` inside the pane.
1266
+ - **What it costs** is its children's sizes, asked only where a track needs
1267
+ them. Fixed tracks, `minmax(0, 1fr)` and a bare count never ask; `1fr`'s
1268
+ minimum is the content floor, which is measured anyway; `auto`,
1269
+ `min-content` and `max-content` columns ask each child once for each width;
1270
+ `auto` rows ask each child its height at its column's width, which placing
1271
+ it needs anyway. What a child answered is remembered until something inside
1272
+ it changes, so a frame in which one card's text changed lays out that card.
1273
+
1274
+ Design notes: [design/architecture/grid-layout.md](../design/architecture/grid-layout.md).
1275
+
1276
+ ## Custom layouts
1277
+
1278
+ `layout` hands a box's children to a layout algorithm in place of flexbox.
1279
+ [Grid](#grid) is one, and the other two built in are:
1280
+
1281
+ ```jsx
1282
+ // a board of notes, each dropped into whichever column is shortest so far
1283
+ <box style={{ layout: { name: 'masonry', columnWidth: 220 }, gap: 12 }}>
1284
+ {notes.map((note) => (
1285
+ <Note
1286
+ key={note.id}
1287
+ note={note}
1288
+ style={note.pinned && { layoutItem: { span: 2 } }}
1289
+ />
1290
+ ))}
1291
+ </box>
1292
+
1293
+ // a dialog's buttons, every one as wide as the widest, at the row's end
1294
+ <box style={{ layout: 'equal-row', gap: 8, justifyContent: 'flex-end' }}>
1295
+ <Button>Cancel</Button>
1296
+ <Button>Save changes</Button>
1297
+ </box>
1298
+ ```
1299
+
1300
+ **`masonry`** is columns, each child placed in the one that is shortest so
1301
+ far: the layout of a photo wall or a board of notes of different heights,
1302
+ which flexbox cannot write — a wrapping row lines its items up in rows. As
1303
+ many `columnWidth`-wide columns as fit (240 by default), or exactly
1304
+ `columns`, sharing the width; `columnGap`, `rowGap` and `gap` space them,
1305
+ and a child's `layoutItem: { span: 2 }` lays it across two.
1306
+
1307
+ **`equal-row`** is a row whose children are all as wide as the widest of
1308
+ them. Flexbox makes siblings equal only by dividing a width it was given
1309
+ (`flex: 1`), and a row sized by its content — a dialog's buttons — has none
1310
+ to divide. Squeezed, the cells shrink together, down to the widest child's
1311
+ content floor and no further. `gap`, `justifyContent` and `alignItems` mean
1312
+ what they mean on a flex row, and `alignItems` defaults to `'stretch'`, so
1313
+ the cells are one height too.
1314
+
1315
+ Things to know:
1316
+
1317
+ - **It runs inside the layout pass**, in the same frame — on a window
1318
+ resize, a live resize, a scroll or a container query flip, with no React
1319
+ render. Measuring in `onLayout` and setting state instead paints the old
1320
+ arrangement first ([react-features.md](react-features.md#measuring-a-node)).
1321
+ - **It is a style property**, so a container query block can switch it:
1322
+ `'@container width < 500': { layout: { name: 'masonry', columns: 1 } }`.
1323
+ - **Right to left is mirrored for you.** An algorithm places from the left,
1324
+ and the renderer mirrors each child across the box when it reads right to
1325
+ left: a masonry fills from the right-hand column.
1326
+ - **Absolutely positioned children** are not the algorithm's: they are
1327
+ placed by their insets against the box's padding box, as anywhere else.
1328
+ - **A child's content floor still holds.** A row inside a masonry card
1329
+ squeezes its label only as far as its longest word, the rule everywhere
1330
+ else ([elements.md](elements.md#everything-shrinks-nothing-shrinks-to-nothing)).
1331
+ - **Not on a scroll pane or a window.** A pane's size is its viewport's,
1332
+ not its content's: put the layout on a `<box>` inside it, which is what it
1333
+ scrolls. It is reported once, and the pane lays out as it would without.
1334
+ - **A layout that throws**, or a name nobody registered, is reported, and
1335
+ the box is laid out as flexbox — in the same frame.
1336
+
1337
+ Writing one of your own is `registerLayout` from `react-x11/host`
1338
+ ([extending.md](extending.md#a-layout-algorithm-of-your-own)). Design notes:
1339
+ [design/architecture/custom-layout.md](../design/architecture/custom-layout.md).
1340
+
1341
+ ## Transitions
1342
+
1343
+ `transition` names how long a change takes. A number covers every animatable
1344
+ property; an object picks them individually.
1345
+
1346
+ ```jsx
1347
+ <box
1348
+ style={{
1349
+ backgroundColor: theme.surface,
1350
+ transition: 120, // or { backgroundColor: 120, left: 200 }
1351
+ ':hover': { backgroundColor: theme.surfaceHover },
1352
+ }}
1353
+ />
1354
+ ```
1355
+
1356
+ Numbers interpolate, and colours interpolate per channel, so any colour the
1357
+ paint accepts animates. Two percentages of the same unit interpolate as
1358
+ numbers — `'-40%'` to `'100%'` is a value travelling across whatever contains
1359
+ it, with nothing measuring the container. A value with no meaningful
1360
+ midpoint — an enum like `flexDirection`, `auto`, a percentage against a
1361
+ pixel value — snaps instead, and `zIndex` never transitions. A duration
1362
+ keyed by a [shorthand](#lengths-and-shorthands) is its longhands', the
1363
+ longhand's own entry winning: `{ border: 150 }` fades the colour
1364
+ `border: '1px solid …'` set, and `{ padding: 200 }` moves each side
1365
+ `padding: '8px 16px'` expanded into.
1366
+
1367
+ The easing is a fixed ease-out cubic. A transition starts from **what is on
1368
+ screen**, not from the declared value, so interrupting one reverses from
1369
+ where it got to rather than jumping to the end first. One to or from
1370
+ [`currentColor`](#currentcolor) runs between the colours as they are at the
1371
+ change.
1372
+
1373
+ A transition runs on the window's own frame clock, which stops the frame the
1374
+ last one lands: nothing polls, and there is no per-widget
1375
+ `requestAnimationFrame`.
1376
+
1377
+ Transitions may animate layout properties, unlike state blocks: an author
1378
+ who writes `transition: { left: 200 }` has asked for animated layout and
1379
+ pays a layout pass per frame for it, and a repaint of whatever that pass
1380
+ moves. `Switch` is the worked example — the thumb is absolutely positioned
1381
+ and slides on `start`, because `justifyContent` would flip between the ends
1382
+ with nothing in between.
1383
+
1384
+ ### Loops
1385
+
1386
+ A transition is over when it arrives. `animation` is the other shape: a
1387
+ property that travels between two values and **keeps doing it**, for the
1388
+ spinner, the pulse, and everything else that means "working, no idea how
1389
+ long".
1390
+
1391
+ ```jsx
1392
+ <box
1393
+ style={{
1394
+ position: 'absolute',
1395
+ start: '-40%',
1396
+ width: '40%',
1397
+ animation: { start: { to: '100%', duration: 1100 } },
1398
+ }}
1399
+ />
1400
+ ```
1401
+
1402
+ Per property, and each one names where it goes and how long a crossing
1403
+ takes:
1404
+
1405
+ | key | |
1406
+ | ----------- | ---------------------------------------------------------------- |
1407
+ | `to` | the far end |
1408
+ | `from` | the near end; **defaults to what the style declares** |
1409
+ | `duration` | one crossing, in ms |
1410
+ | `easing` | `'linear'` (default), `'ease-in'`, `'ease-out'`, `'ease-in-out'` |
1411
+ | `alternate` | turn around at each end instead of wrapping back to `from` |
1412
+ | `delay` | when the loop's own time starts, in ms; negative starts it early |
1413
+
1414
+ `from` defaulting to the declared value is what makes a pulse read as a
1415
+ resting colour plus somewhere to go:
1416
+
1417
+ ```jsx
1418
+ style={{
1419
+ backgroundColor: theme.track,
1420
+ animation: {
1421
+ backgroundColor: { to: theme.accent, duration: 900, alternate: true },
1422
+ },
1423
+ }}
1424
+ ```
1425
+
1426
+ That value is also **where the property rests whenever the loop is not
1427
+ running** — before the first frame, off screen, or under reduced motion — so
1428
+ a loop never leaves a node with no value for the thing it animates.
1429
+
1430
+ Every loop starts when its node gets the style, so loops with one duration
1431
+ move together for good. `delay` is how they stop doing that — CSS's
1432
+ `animation-delay`. A positive delay holds `from` that long before the first
1433
+ crossing, once rather than every cycle; a negative one starts the loop that
1434
+ far in, already moving, which is the way to say a phase:
1435
+
1436
+ ```jsx
1437
+ const TypingDot = ({ index }) => (
1438
+ <box
1439
+ style={{
1440
+ width: 6,
1441
+ height: 6,
1442
+ borderRadius: 3,
1443
+ backgroundColor: theme.textMuted,
1444
+ opacity: 0.3,
1445
+ animation: {
1446
+ opacity: { to: 1, duration: 500, alternate: true, delay: -index * 160 },
1447
+ },
1448
+ }}
1449
+ />
1450
+ );
1451
+ ```
1452
+
1453
+ A changed `delay` is a different loop and starts from the top, as a changed
1454
+ `duration` does.
1455
+
1456
+ A loop's easing is `linear` unless you say otherwise, where a transition's is
1457
+ ease-out: a cycle that restarts would stutter at the wrap.
1458
+
1459
+ **A loop costs a repaint of what moves**, on the window's frame clock: an
1460
+ indeterminate progress bar repaints the bar, not the window. Prefer it to
1461
+ `setInterval` → `setState`, which re-renders the component on a cadence
1462
+ unrelated to when the window can present.
1463
+
1464
+ On macOS a loop — or a transition — on a plain box's colour, border width
1465
+ or radius runs in Core Animation and costs no frames at all. So does a loop
1466
+ on an inset of a box that is `position: 'absolute'` and
1467
+ `pointerEvents: 'none'`, as the block of `<ProgressBar indeterminate>` is;
1468
+ without `pointerEvents: 'none'` it stays on the frame clock, because input is
1469
+ hit where the layout put the box. Anything else runs as above.
1470
+ `examples/animation.jsx` has both shapes side by side, with a frame counter
1471
+ in the terminal that shows the difference.
1472
+
1473
+ **And it stops itself** whenever it goes off the screen:
1474
+
1475
+ - the window is unmapped, minimized, or fully obscured under a bare WM;
1476
+ - anything above the node hides it — `display: 'none'`, `<Suspense>`,
1477
+ `<Activity>`;
1478
+ - the node unmounts, or the style stops declaring the loop;
1479
+ - the desktop asked for **reduced motion**
1480
+ ([system.md](system.md#usedesktopsettings--how-the-desktop-wants-an-app-to-feel));
1481
+ - all the loop can draw is out of sight — scrolled out of its pane, clipped
1482
+ away by a box above it, off the window.
1483
+
1484
+ Each one leaves the frame clock idle, and every one of them runs the loop
1485
+ again when it goes away — with its phase reset.
1486
+
1487
+ Out of sight is judged after each layout pass, by what **every** frame of
1488
+ the loop can reach and never by where the current one has the node: the
1489
+ block of `<ProgressBar indeterminate>` starts each crossing off its track,
1490
+ and is judged by the track it slides in. A loop on a colour, an opacity or
1491
+ a radius reaches what the node and its children cover. One on the outline,
1492
+ or one that moves or sizes a node out of the flow (`position: 'absolute'`),
1493
+ reaches the nearest box that clips it, or the whole window when nothing
1494
+ does. And one that moves or sizes a node in the flow is never out of sight
1495
+ at all: each of its frames can move the node's siblings and resize the
1496
+ boxes above it, so it runs for as long as its window is up.
1497
+
1498
+ Reduced motion is honoured in core, once, for every loop in every
1499
+ application: `Gtk/EnableAnimations` off means loops do not start. What core
1500
+ cannot decide is what the _still_ frame should look like, which is the
1501
+ author's — `<ProgressBar indeterminate>` parks its block inside the track
1502
+ rather than at its off-screen starting point, so a bar that cannot move
1503
+ still shows something in progress.
1504
+
1505
+ Nothing about this is a per-frame callback: `animation` describes the motion
1506
+ and the renderer runs it. For a `<glarea>` drawing its own frames,
1507
+ [`frameLoop`](gl.md) is the per-frame seam; there is no 2D one.
1508
+
1509
+ ## Window size queries
1510
+
1511
+ `'@width >= 600'` and friends are the X11 analogue of `@media`: what a style
1512
+ can usefully ask about here is the window it is laid out in, not the screen.
1513
+
1514
+ ```jsx
1515
+ const s = createStyles({
1516
+ bar: {
1517
+ flexDirection: 'column',
1518
+ gap: 4,
1519
+ '@width >= 600': { flexDirection: 'row', gap: 16 },
1520
+ },
1521
+ });
1522
+ ```
1523
+
1524
+ `width` and `height`, with `>=`, `<=`, `>` or `<`. Blocks that match are
1525
+ merged in declaration order, before state blocks — so a `:hover` inside the
1526
+ wide layout still wins over the wide layout.
1527
+
1528
+ **A size query may set layout properties, unlike a state block.** It is only
1529
+ re-evaluated inside the layout pass a resize already requires, and only when
1530
+ the size actually changed, so it costs nothing extra.
1531
+
1532
+ A malformed query is an error rather than a key that silently never
1533
+ matches.
1534
+
1535
+ ## Container queries
1536
+
1537
+ `'@container width >= 400'` asks about the box a node is inside rather than
1538
+ the window it is in — CSS's `@container`, for the case a window query cannot
1539
+ tell apart: the same card in a 220px sidebar and in a 600px pane, in one
1540
+ 900px window.
1541
+
1542
+ ```jsx
1543
+ const s = createStyles({
1544
+ pane: { container: true, flexGrow: 1, minWidth: 0, overflow: 'scroll' },
1545
+ card: {
1546
+ flexDirection: 'column',
1547
+ gap: 8,
1548
+ '@container width >= 400': { flexDirection: 'row', gap: 16 },
1549
+ },
1550
+ });
1551
+
1552
+ <box style={s.pane}>
1553
+ <box style={s.card}>…</box>
1554
+ </box>;
1555
+ ```
1556
+
1557
+ A container is declared with the **`container` style property** — CSS's
1558
+ `container-type` and `container-name` in one — and a block answers to the
1559
+ **nearest ancestor that declares one**. Wrapping the card in another `<box>`
1560
+ for a border changes nothing: the parent is not the container, the declared
1561
+ ancestor is, which is what lets a component be put anywhere and still get
1562
+ the answer its author meant. A scroll pane is not a container unless it
1563
+ declares one, though it is the ideal one to declare. A node's blocks never
1564
+ answer to its own size, even when it declares itself a container: its
1565
+ declaration is for the blocks under it. The declaration changes nothing
1566
+ about the container itself; it is neither layout nor paint. Being a style
1567
+ property, it can be set from inside a query block — a box that is a
1568
+ container only in the wide layout — and `false` takes a declaration back
1569
+ from earlier in a style array.
1570
+
1571
+ `width` and `height`, with `>=`, `<=`, `>` or `<`, in the logical pixels the
1572
+ dependent's own lengths are written in. Blocks that match are merged in
1573
+ declaration order with the other query blocks, before the state blocks.
1574
+
1575
+ ### Named containers
1576
+
1577
+ `container: 'pane'` names the container, and `'@container pane width >= 300'`
1578
+ asks for it by name — **reaching past** any nearer container. A card that is
1579
+ a container for its own contents does not capture a query a label inside it
1580
+ meant for the pane:
1581
+
1582
+ ```jsx
1583
+ const s = createStyles({
1584
+ pane: { container: 'pane', flexGrow: 1, minWidth: 0, overflow: 'scroll' },
1585
+ card: {
1586
+ container: true, // the card's contents ask about the card…
1587
+ '@container width >= 380': { flexDirection: 'row' }, // …and the card asks the pane
1588
+ },
1589
+ detail: { '@container width < 220': { display: 'none' } }, // the card
1590
+ label: {
1591
+ display: 'none',
1592
+ '@container pane width >= 300': { display: 'flex' }, // the pane, past the card
1593
+ },
1594
+ });
1595
+ ```
1596
+
1597
+ `examples/container-queries.jsx` is that design in full: one `<Card>` in a
1598
+ sidebar and in a main pane, both panes named `pane`, so the labels come and
1599
+ go with whichever pane a card is in.
1600
+
1601
+ A name nothing above carries makes the block **not apply, quietly** — that
1602
+ is what lets one component render inside and outside a `sidebar`. An
1603
+ **unnamed** query with no container above it is reported in development,
1604
+ the way an unknown token is: the window already has a spelling (`'@width'`),
1605
+ so the only thing a silent fallback to it could hide is a forgotten
1606
+ declaration. `REACT_X11_STRICT_TOKENS=1` makes the report a throw.
1607
+
1608
+ ### When it is answered, and what it costs
1609
+
1610
+ Like a size query, a container block **may set layout properties**, and for
1611
+ the same reason: it is only ever re-evaluated inside a layout pass. Unlike
1612
+ one, it cannot be answered _before_ the pass — a container's size is what
1613
+ the pass produces — so the blocks are resolved after it, and the tree is laid
1614
+ out once more when an answer moved. A frame where no answer crosses a
1615
+ threshold (a scroll, a hover, a resize between two breakpoints) pays one
1616
+ size read per container and nothing else. A frame where one does pays what
1617
+ the same change would have cost had React made it.
1618
+
1619
+ When what depends on the size is not a style — how many rows to build, which
1620
+ component to render — the same size reaches React through `onLayout` on any
1621
+ element ([react-features.md](react-features.md#measuring-a-node)). It
1622
+ arrives after the pass, so the reaction lands a frame later than a block
1623
+ would; that is the reason to write a style as a block where one can.
1624
+
1625
+ ### A block must not move the size it asks about
1626
+
1627
+ Yoga has no size containment, so here this is a rule, detected rather than
1628
+ prevented. A block that grows the container past the very threshold that
1629
+ turned it on — and shrinks it back when it turns off — has no size that
1630
+ satisfies it. The renderer notices, **holds the answer it has**, and says so
1631
+ once in development, naming the node. The answer stays held until the
1632
+ container's size moves for some other reason.
1633
+
1634
+ The way to stay clear of it is also what makes a good container: a size that
1635
+ does not come from the content below it.
1636
+
1637
+ | | |
1638
+ | ------------------------------------------------ | ----------------------------------------------------------------- |
1639
+ | an explicit `width` / `height` | fixed, whatever is inside |
1640
+ | a flex item with `minWidth: 0` and a `flexBasis` | the parent distributes it; `flex: 1` is both in one |
1641
+ | `overflow: 'scroll'` | **the natural container**: content overflows rather than grows it |
1642
+
1643
+ The common responsive pattern — less content when narrow, more when wide —
1644
+ is monotone and settles on the first re-layout; a warning means the design
1645
+ genuinely contradicts itself.
1646
+
1647
+ Design notes: [design/architecture/container-queries.md](../design/architecture/container-queries.md).
1648
+
1649
+ ## Capability queries
1650
+
1651
+ `'@supports transparency'` is to `@supports` what the size queries are to
1652
+ `@media`: what a style can ask about the **server**, rather than about the
1653
+ window it is in.
1654
+
1655
+ ```jsx
1656
+ const s = createStyles({
1657
+ menu: {
1658
+ backgroundColor: '#1c1c22', // works everywhere
1659
+ borderWidth: 1,
1660
+ borderColor: '#3a3a44',
1661
+ '@supports transparency': {
1662
+ backgroundColor: 'rgba(24, 24, 30, 0.86)',
1663
+ borderRadius: 14,
1664
+ borderWidth: 0,
1665
+ },
1666
+ },
1667
+ });
1668
+ ```
1669
+
1670
+ Write the design that works everywhere as the base and the enhancement in
1671
+ the block — the same way you would write a `@media` query for a wider
1672
+ screen. Get it the other way round and the fallback is the _unusual_ case,
1673
+ which is the one nobody tests.
1674
+
1675
+ `transparency` is true only when **both** halves hold: the window was
1676
+ created on a 32-bit visual (`<window transparent>`, and the display had one
1677
+ to give), **and** a compositor is running to blend it. Either half missing
1678
+ and a transparent corner is a _black_ corner — X shows the raw pixels — so
1679
+ the answer is no, and the block does not apply.
1680
+
1681
+ It is answered **per window**, not per display. A component rendered inside
1682
+ a `<popup transparent>` gets the translucent design; the same component
1683
+ nested in a plain `<window>` gets the opaque one, without being told which
1684
+ it is in.
1685
+
1686
+ The answer can change while the app runs — a compositor being started or
1687
+ stopped is a checkbox on some desktops — and blocks are re-resolved when it
1688
+ does. That is why `transparent` takes the 32-bit visual even when nothing is
1689
+ compositing yet: a window's _visual_ is fixed at creation and cannot follow,
1690
+ but what it paints can.
1691
+
1692
+ For decisions that are not styling — sizing a popup to hold a shadow margin,
1693
+ say, which has to happen before the window exists — there is
1694
+ [`useSupports('transparency')`](elements.md#transparent--rounded-corners-and-translucency),
1695
+ which answers the same question about the display.
1696
+
1697
+ An unknown feature name is an error, like a malformed size query.
1698
+
1699
+ Running with `REACT_X11_NO_TRANSPARENCY=1` makes the answer false on any
1700
+ display, so the base design can be looked at without stopping the compositor
1701
+ for the whole session. See
1702
+ [debugging.md](debugging.md#react_x11_no_transparency1).
1703
+
1704
+ ## Elements that are not styled
1705
+
1706
+ The declarative SVG children carry a vocabulary of their own — `<circle cx
1707
+ r fill>` is SVG's, not the style channel's — so the style channel does not
1708
+ apply to them, the same way it does not apply to an `<input type>` in the
1709
+ DOM. They report
1710
+ `stylable === false` and their props are passed through untouched.