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,194 @@
1
+ # Headless components
2
+
3
+ A library that returns DOM prop bags — `useButton()` and the like — is
4
+ DOM-coupled with the rendering deferred, not headless in a sense that helps
5
+ here. The two packages below own state and collections, return plain values,
6
+ and never touch a ref. Headless UI and the Radix primitives render DOM hosts
7
+ and fail ([register](../ecosystem.md#what-does-not-work)); downshift's
8
+ `getInputProps()` works on a `<textinput>` ([forms](forms.md)).
9
+
10
+ For finished widgets react-x11 ships its own: `Button`, `Checkbox`,
11
+ `Radio`/`RadioGroup`, `Switch`, `ProgressBar`, `Select`, `Slider`, `Tooltip`,
12
+ `Dialog`, `MenuBar`/`ContextMenu`, `Tabs`, `Table`, `SplitPane` — see
13
+ [components](../components.md).
14
+
15
+ ## react-stately {#react-stately}
16
+
17
+ **Out of the box.** react-stately@3.48.0.
18
+
19
+ The state-machine half of React Aria: `useListState`, `useTreeState`,
20
+ `useTableState`, `useSelectState`, `useToggleState`, `useSliderState`,
21
+ `useComboBoxState`, `useMenuTriggerState`, `useNumberFieldState` and about
22
+ thirty-five more, plus the `Item`/`Section` collection API. Each hook owns
23
+ selection, disabled-key traversal, expansion and typeahead-ready collections
24
+ — everything but rendering and event binding, which react-x11 supplies. No
25
+ hook references `document`, `window` or `getBoundingClientRect` at runtime,
26
+ and the dependencies (`@internationalized/*`, `@react-types/shared`,
27
+ `use-sync-external-store`) are DOM-free.
28
+
29
+ ```jsx
30
+ import React from 'react';
31
+ import { useListState, Item } from 'react-stately';
32
+
33
+ function Picker() {
34
+ const state = useListState({
35
+ children: [
36
+ <Item key="alpha">Alpha</Item>,
37
+ <Item key="beta">Beta</Item>,
38
+ <Item key="gamma">Gamma</Item>,
39
+ ],
40
+ selectionMode: 'multiple',
41
+ });
42
+ const mgr = state.selectionManager;
43
+ return (
44
+ <box
45
+ style={{ flexDirection: 'column' }}
46
+ focusable
47
+ onKeyDown={(ev) => {
48
+ if (ev.key === 'ArrowDown')
49
+ mgr.setFocusedKey(
50
+ state.collection.getKeyAfter(mgr.focusedKey) ?? mgr.focusedKey,
51
+ );
52
+ if (ev.codepoint === 32) mgr.toggleSelection(mgr.focusedKey);
53
+ }}
54
+ >
55
+ {[...state.collection].map((item) => (
56
+ <box
57
+ key={item.key}
58
+ style={{ height: 20, justifyContent: 'center' }}
59
+ onClick={() => mgr.toggleSelection(item.key)}
60
+ >
61
+ <text>
62
+ {(mgr.isSelected(item.key) ? '* ' : ' ') + item.rendered}
63
+ </text>
64
+ </box>
65
+ ))}
66
+ </box>
67
+ );
68
+ }
69
+ ```
70
+
71
+ - **Only `react-stately` is portable.** `react-aria` (hooks that produce DOM
72
+ props and bind DOM events) and `react-aria-components` are not — do not
73
+ follow Adobe's docs into `useListBox` or `useButton`.
74
+ - Since 3.48 the old `@react-stately/*` packages are bundled in, with
75
+ per-hook subpath exports (`react-stately/useListState`) for lean imports.
76
+ - The collection API wants `<Item>`/`<Section>` elements, or `items` plus a
77
+ render function — ceremonious for a three-entry menu.
78
+ - `useAsyncList` carries fetch-based loading state — fine under Node, the
79
+ one hook with I/O opinions.
80
+ - Drag-and-drop hooks (`useDraggableCollectionState`) hold state, but
81
+ everything that would _feed_ them events is DOM-side; write the event
82
+ plumbing yourself.
83
+
84
+ ## `@tanstack/react-table` {#tanstack-table}
85
+
86
+ **Out of the box.** @tanstack/react-table@9.1.2 (table-core 9.1.2,
87
+ react-store 0.11.1).
88
+
89
+ A headless table engine: column defs, row models, sorting, filtering,
90
+ grouping, pagination, column sizing, visibility and pinning, as pure state
91
+ plus derived row models. You map `getHeaderGroups()` and
92
+ `getRowModel().rows` to react-x11 elements.
93
+
94
+ v9's "adapters" are per framework, not per renderer: `@tanstack/react-table`
95
+ _is_ the React adapter, so there is nothing to write, and it never touches
96
+ `react-dom` or `document` at runtime (but see the react-dom and `flexRender`
97
+ bullets). `column.getToggleSortingHandler()` goes straight onto a header
98
+ `onClick`. Features and row models are registered up front through
99
+ `tableFeatures`, and the hook is `useTable`.
100
+
101
+ ```jsx
102
+ import React from 'react';
103
+ import {
104
+ useTable,
105
+ tableFeatures,
106
+ rowSortingFeature,
107
+ createCoreRowModel,
108
+ createSortedRowModel,
109
+ createColumnHelper,
110
+ flexRender,
111
+ } from '@tanstack/react-table';
112
+
113
+ // Registered once, outside the component. A feature that is not listed here
114
+ // does not exist: its state slice, its column methods, its row model.
115
+ const features = tableFeatures({
116
+ rowSortingFeature,
117
+ coreRowModel: createCoreRowModel(),
118
+ sortedRowModel: createSortedRowModel(),
119
+ });
120
+
121
+ const col = createColumnHelper();
122
+ const columns = [
123
+ col.accessor('name', { header: 'Name' }),
124
+ col.accessor('port', { header: 'Port', cell: (i) => `:${i.getValue()}` }),
125
+ ];
126
+
127
+ function Servers({ data }) {
128
+ const table = useTable({ features, columns, data });
129
+ return (
130
+ <box style={{ flexDirection: 'column' }}>
131
+ {table.getHeaderGroups().map((hg) => (
132
+ <box key={hg.id} style={{ flexDirection: 'row', height: 20 }}>
133
+ {hg.headers.map((h) => (
134
+ <box
135
+ key={h.id}
136
+ style={{ width: 120 }}
137
+ onClick={h.column.getToggleSortingHandler()}
138
+ >
139
+ <text>
140
+ {flexRender(h.column.columnDef.header, h.getContext())}
141
+ </text>
142
+ </box>
143
+ ))}
144
+ </box>
145
+ ))}
146
+ {table.getRowModel().rows.map((row) => (
147
+ <box key={row.id} style={{ flexDirection: 'row', height: 20 }}>
148
+ {row.getAllCells().map((cell) => (
149
+ <text key={cell.id} style={{ width: 120 }}>
150
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
151
+ </text>
152
+ ))}
153
+ </box>
154
+ ))}
155
+ </box>
156
+ );
157
+ }
158
+ ```
159
+
160
+ Sorting is uncontrolled here — the table's store holds it, and the header
161
+ click re-renders through the adapter's subscription. Pass
162
+ `state`/`onSortingChange` to control it, or a selector as `useTable`'s
163
+ second argument to subscribe a component to only some slices. Render into
164
+ the built-in `Table` component or raw `<box style={{ flexDirection: 'row' }}>`
165
+ rows.
166
+
167
+ - **`coreRowModel` is opt-in, and forgetting it fails silently.** Without
168
+ `coreRowModel: createCoreRowModel()` in `tableFeatures`, headers render
169
+ and `getRowModel().rows` is `[]`, with no error. The official examples get
170
+ it from framework presets; register it yourself.
171
+ - **npm installs a `react-dom` you must not keep.** `@tanstack/react-store`
172
+ declares a `react-dom` _peer_, which npm 7+ auto-installs. The shipped
173
+ code never imports it (test 2 finds one `"button"`, in a doc comment, and
174
+ the table works with `node_modules/react-dom` deleted), but a resident
175
+ react-dom is the [silent-failures](../ecosystem.md#silent-failures)
176
+ hazard. Install with `--legacy-peer-deps`, or delete it so
177
+ `npm ls react-dom` stays empty.
178
+ - **`flexRender` returns a React element even for a plain string cell** (v8
179
+ returned the string). Inside `<text>` it paints identically, but a test or
180
+ a11y walk reading a `<text>` node's `children` prop sees an element
181
+ object; read the rendered chunks. A `cell` that returns DOM elements
182
+ throws the unknown-element error; return strings or react-x11 elements.
183
+ - Column _resizing_ helpers (`header.getResizeHandler`) expect DOM mouse or
184
+ touch events with `clientX` and fall back to a `document`-listener drag
185
+ loop (injectable as `_contextDocument`). Simpler: handle the drag off
186
+ `onMouseDown`/`onMouseMove` (react-x11 events carry `x`/`y`) and call
187
+ `table.setColumnSizing` (needs `columnSizingFeature` registered).
188
+ - `useTable` probes `typeof window` to pick `useLayoutEffect` and gets
189
+ `useEffect` under Node — a harmless test-3 hit (timing, not a feature
190
+ gate).
191
+ - For thousands of rows, pair it with a
192
+ [virtualizer](layout.md#tanstack-virtual).
193
+ - State updates driven from outside a React event land one frame later than
194
+ the call, as with any react-x11 update.
@@ -0,0 +1,86 @@
1
+ # Internationalization
2
+
3
+ ## i18next + react-i18next
4
+
5
+ **Out of the box.** i18next@26.3.6, react-i18next@17.0.11.
6
+
7
+ i18next (resources, interpolation, plurals, namespaces, backends) with
8
+ react-i18next's `useTranslation`, `<Trans>` and provider-driven re-render
9
+ runs unmodified: interpolation, count-based plurals, `<Trans>`, and
10
+ `i18n.changeLanguage('de')` re-rendering the mounted tree in place. Node
11
+ ships full ICU, so `Intl.NumberFormat` and `Intl.DateTimeFormat` work too.
12
+
13
+ Two react-x11-specific rules:
14
+
15
+ 1. **`<Trans>` component maps must use react-x11 elements.**
16
+ `components={{ 1: <span/> }}` throws
17
+ `react-x11: <span> is not allowed inside <text>` — inside a `<text>`, only
18
+ strings and nested `<text>` spans are legal. Use `<text style={...}/>` as
19
+ the wrapper.
20
+ 2. **Turn off HTML escaping.** i18next escapes interpolated values for an
21
+ HTML sink by default, and react-x11 draws raw glyphs, so `O'Brien` renders
22
+ as `O&#39;Brien`. Set `interpolation: { escapeValue: false }`.
23
+
24
+ ```jsx
25
+ import React from 'react';
26
+ import i18next from 'i18next';
27
+ import {
28
+ I18nextProvider,
29
+ initReactI18next,
30
+ useTranslation,
31
+ Trans,
32
+ } from 'react-i18next';
33
+
34
+ const i18n = i18next.createInstance();
35
+ await i18n.use(initReactI18next).init({
36
+ lng: 'en',
37
+ resources: {
38
+ en: {
39
+ translation: {
40
+ greeting: 'Hello, {{name}}!',
41
+ items_one: '{{count}} item',
42
+ items_other: '{{count}} items',
43
+ rich: 'Press <1>Enter</1> to save',
44
+ },
45
+ },
46
+ },
47
+ interpolation: { escapeValue: false }, // no HTML sink to escape for
48
+ });
49
+
50
+ function Status() {
51
+ const { t } = useTranslation();
52
+ return (
53
+ <box style={{ flexDirection: 'column' }}>
54
+ <text>{t('greeting', { name: 'X11' })}</text>
55
+ <text>{t('items', { count: 2 })}</text>
56
+ <text>
57
+ <Trans
58
+ i18nKey="rich"
59
+ components={{ 1: <text style={{ color: '#e00' }} /> }}
60
+ />
61
+ </text>
62
+ </box>
63
+ );
64
+ }
65
+
66
+ // mount under <I18nextProvider i18n={i18n}> inside your <window>;
67
+ // i18n.changeLanguage('de') re-renders live
68
+ ```
69
+
70
+ To load translations from disk use `i18next-fs-backend`;
71
+ `i18next-http-backend` also works, since Node has `fetch`.
72
+
73
+ ## What is missing below the string
74
+
75
+ React-x11 gaps rather than i18next ones, and they matter more than the
76
+ library choice:
77
+
78
+ - **No input method on X11 or Cocoa.** Dead keys and the Compose key are
79
+ handled in the renderer ([events.md](../events.md#composition)), but
80
+ there is no XIM/IBus/Fcitx client on X11 and no `NSTextInputClient` on
81
+ Cocoa, so a language that needs one — CJK — can be _displayed_ but not
82
+ typed there. The Wayland and Windows backends drive the system input
83
+ method.
84
+ - `<Trans>` with `<strong>`/`<em>`-style numbered tags is fine as long as
85
+ every referenced component is a `<text>` span. An accidental DOM tag fails
86
+ loudly at render.
@@ -0,0 +1,377 @@
1
+ # Icons
2
+
3
+ react-x11 has a real `<svg>` host, so icons get further than most DOM
4
+ libraries. Two renderer facts shape every entry on this page:
5
+
6
+ 1. **ntk's `SvgView`, which rasterizes `<svg>`, ignores presentation
7
+ attributes on the root element.** Inheritance starts from its own
8
+ defaults, so an icon with `fill="none" stroke="currentColor"
9
+ stroke-width="2"` on the root renders as solid black blobs — wrong
10
+ glyphs, not just the wrong colour. Moving those attributes onto an inner
11
+ `<g>`, which does inherit, fixes it.
12
+ 2. **Flat style props on `<svg>` throw.** `<svg width={24}>` is
13
+ `react-x11: <svg width=…> is a style property — pass it in style`. Size
14
+ with `style={{ width: 24 }}`; the `viewBox` keeps the aspect ratio.
15
+
16
+ Fact 2 is why `lucide-react`, `@tabler/icons-react` and
17
+ `@phosphor-icons/react` cannot work
18
+ ([register](../ecosystem.md#render-time)); `@heroicons/react` is the
19
+ exception.
20
+
21
+ ## The `iconSource` wrapper {#iconsource}
22
+
23
+ Four of the packages below ship whole SVG _strings_ with paint attributes on
24
+ the root. They all need the same twelve lines:
25
+
26
+ ```js
27
+ // icon-source.js — re-root an icon's paint attributes onto a <g> so they
28
+ // survive SvgView, and pin `color` there so currentColor resolves.
29
+ const ROOT = /<svg([^>]*)>([\s\S]*)<\/svg>\s*$/;
30
+
31
+ export function iconSource(svgText, color = '#000') {
32
+ const [, root, body] = ROOT.exec(svgText);
33
+ const viewBox = /viewBox="[^"]*"/.exec(root)?.[0] ?? '';
34
+ const paint = (
35
+ root.match(
36
+ /(?:fill|stroke|stroke-width|stroke-linecap|stroke-linejoin|fill-rule|clip-rule)="[^"]*"/g,
37
+ ) ?? []
38
+ ).join(' ');
39
+ return `<svg xmlns="http://www.w3.org/2000/svg" ${viewBox}><g color="${color}" ${paint}>${body}</g></svg>`;
40
+ }
41
+ ```
42
+
43
+ `currentColor` resolves to the inherited `color`, which defaults to `#000`,
44
+ so the `color` attribute on the `<g>` is what makes recolouring work.
45
+ `svgText.replaceAll('currentColor', color)` alone is _not_ enough: it fixes
46
+ the colour but not the dropped `fill="none"`, so stroke icons still paint as
47
+ filled blobs. Cache the wrapped strings per (name, style, colour).
48
+
49
+ ## Iconify {#iconify}
50
+
51
+ **Out of the box.** @iconify/utils@3.1.4 with any `@iconify-json/*` set.
52
+
53
+ Iconify publishes every major icon set as JSON data — `@iconify-json/tabler`,
54
+ `@iconify-json/mdi`, `@iconify-json/ph`, `@iconify-json/lucide`,
55
+ `@iconify-json/heroicons` — plus `@iconify/utils`, which looks an icon up and
56
+ builds SVG markup from it. The highest-leverage route: one pipeline, 200,000+
57
+ icons, and the only string-SVG route that needs **no re-rooting adapter**,
58
+ because `iconToSVG` returns the body separately from the root attributes and
59
+ the paint attributes live inside the body.
60
+
61
+ ```jsx
62
+ import { getIconData, iconToSVG, iconToHTML, replaceIDs } from '@iconify/utils';
63
+ import { icons as tabler } from '@iconify-json/tabler';
64
+
65
+ function iconSvg(name, color = '#000') {
66
+ const data = getIconData(tabler, name); // null if unknown
67
+ const built = iconToSVG(data, { height: 24 });
68
+ return iconToHTML(replaceIDs(built.body), built.attributes).replaceAll(
69
+ 'currentColor',
70
+ color,
71
+ );
72
+ }
73
+
74
+ function Icon({ name, size = 24, color }) {
75
+ return <svg source={iconSvg(name, color)} style={{ width: size }} />;
76
+ }
77
+
78
+ // <Icon name="home" color="#7c3aed" />
79
+ // <Icon name="settings" size={16} color="#0891b2" />
80
+ ```
81
+
82
+ - `replaceIDs` matters for icons with gradients or clip paths, whose `id`s
83
+ would collide inside one rasterized document. Harmless across separate
84
+ `<svg>` nodes, but free to keep.
85
+ - Pick per-set packages (`@iconify-json/tabler` is about 5,000 icons in one
86
+ JSON) rather than the monolithic `@iconify/json`, which is all 200,000+ and
87
+ roughly 100 MB.
88
+ - There are no events inside a rasterized SVG. Wrap it in a
89
+ `<box onClick>`.
90
+
91
+ ## `@heroicons/react` {#heroicons-react}
92
+
93
+ **Out of the box — size it with `style`.** @heroicons/react@2.2.0.
94
+
95
+ The exception among the `*-react` icon packages: its components size
96
+ themselves with a Tailwind class name rather than flat `width`/`height`
97
+ attributes, so the `<svg>` host accepts them. lucide-react,
98
+ `@tabler/icons-react` and `@phosphor-icons/react` all throw.
99
+
100
+ ```jsx
101
+ import { BeakerIcon } from '@heroicons/react/24/solid';
102
+
103
+ <box style={{ flexGrow: 1 }}>
104
+ <BeakerIcon style={{ width: 24, height: 24 }} />
105
+ </box>;
106
+ ```
107
+
108
+ The renderer ignores the `className`, so pass `style={{ width, height }}`
109
+ yourself — an icon with no size lays out at its natural size or collapses,
110
+ depending on the surrounding flex box. There is no recolour channel; for a
111
+ themed icon use the asset package below, or `@iconify-json/heroicons`.
112
+
113
+ ## `lucide` {#lucide}
114
+
115
+ **Adapter, ~10 lines.** lucide@1.28.0.
116
+
117
+ Lucide's core package exports every icon as plain data — an array of
118
+ `[tag, attrs]` pairs — alongside the DOM-oriented `createIcons`.
119
+ Tree-shakeable per icon, and the best fit for react-x11: no string parsing,
120
+ each pair becomes a JSX child of `<svg>` (camelCase props serialize as
121
+ kebab-case attributes).
122
+
123
+ ```jsx
124
+ import { createElement } from 'react';
125
+ import { House, Sun } from 'lucide';
126
+
127
+ function Icon({ icon, size = 24, color = '#000', strokeWidth = 2 }) {
128
+ return (
129
+ <svg viewBox="0 0 24 24" style={{ width: size, height: size }}>
130
+ <g
131
+ fill="none"
132
+ stroke={color}
133
+ strokeWidth={strokeWidth}
134
+ strokeLinecap="round"
135
+ strokeLinejoin="round"
136
+ >
137
+ {icon.map(([tag, attrs], i) =>
138
+ createElement(tag, { key: i, ...attrs }),
139
+ )}
140
+ </g>
141
+ </svg>
142
+ );
143
+ }
144
+
145
+ // <Icon icon={House} color="#16a34a" />
146
+ // <Icon icon={theme === 'dark' ? Sun : House} color={accent} />
147
+ ```
148
+
149
+ Swapping `icon` or `color` in state repaints the icon.
150
+
151
+ - Omitting the `<g>` and putting `stroke`/`fill` on the `<svg>` silently
152
+ renders black filled blobs.
153
+ - Function props on SVG children are dropped by the serializer, so there are
154
+ no per-child events. Wrap the `<svg>` in a `<box onClick>` for a clickable
155
+ icon.
156
+ - `className` on a child serializes to a `class-name` attribute — harmless,
157
+ but do not rely on classes for styling.
158
+
159
+ ## `lucide-static` {#lucide-static}
160
+
161
+ **Adapter — the [`iconSource` wrapper](#iconsource).** lucide-static@1.28.0.
162
+
163
+ Every Lucide icon as a named export holding an SVG markup string: 24×24,
164
+ stroke-based, `stroke="currentColor"` with round caps and joins — all on the
165
+ root element, hence the wrapper.
166
+
167
+ ```jsx
168
+ import * as lucide from 'lucide-static';
169
+ import { iconSource } from './icon-source.js';
170
+
171
+ const Icon = ({ name, size = 24, color = '#000' }) => (
172
+ <svg source={iconSource(lucide[name], color)} style={{ width: size }} />
173
+ );
174
+
175
+ // <Icon name="House" color="#e11d48" />
176
+ // <Icon name="Sun" size={16} color="#eab308" />
177
+ ```
178
+
179
+ Prefer the `lucide` data package; use this one when you want the markup
180
+ string itself (asset pipelines, a pre-built sheet).
181
+
182
+ ## `react-icons` {#react-icons}
183
+
184
+ **Adapter, ~15 lines.** react-icons@5.7.0.
185
+
186
+ The React icon aggregator — Font Awesome, Material, Lucide, Bootstrap,
187
+ Simple Icons and about fifty more behind one API. Unadapted it throws,
188
+ because `IconBase` always sets `height`/`width` (default `"1em"`) as flat
189
+ props on `<svg>`.
190
+
191
+ The adapter sidesteps `IconBase`. An icon component called with empty props
192
+ returns the `IconBase` element, whose props carry `attr` (the set's root
193
+ attributes — `viewBox`, and for stroke sets the paint attributes) and
194
+ `children` (the paths as React elements). Re-rooting `attr` onto an inner
195
+ `<g>` handles fact 1.
196
+
197
+ ```jsx
198
+ import { FaHeart } from 'react-icons/fa';
199
+ import { LuHouse } from 'react-icons/lu';
200
+
201
+ // Works for both fill sets (fa, md, …) and stroke sets (lu, fi, …).
202
+ function RIcon({ icon: I, size = 24, color = '#000' }) {
203
+ const { attr = {}, children } = I({}).props; // IconBase element
204
+ const { viewBox = '0 0 24 24', ...paint } = attr;
205
+ return (
206
+ <svg viewBox={viewBox} style={{ width: size, height: size }}>
207
+ <g
208
+ color={color}
209
+ fill="currentColor"
210
+ stroke="currentColor"
211
+ strokeWidth={0}
212
+ {...paint}
213
+ >
214
+ {children}
215
+ </g>
216
+ </svg>
217
+ );
218
+ }
219
+
220
+ // <RIcon icon={FaHeart} color="#e11d48" /> fill-based
221
+ // <RIcon icon={LuHouse} color="#2563eb" /> stroke-based
222
+ ```
223
+
224
+ - `I({})` relies on each icon being a plain function component wrapping
225
+ `GenIcon` — react-icons' shape since v4, but an internal one. `lucide` and
226
+ the Iconify pipeline have no such seam.
227
+ - `IconContext` (global size/colour config) is bypassed; pass props instead.
228
+ - The `title` prop emits an SVG `<title>` child, which reaches no tooltip or
229
+ assistive technology from a rasterized SVG. Omit it.
230
+ - react-icons bundles every set and is heavy on disk; for one style,
231
+ `lucide` or `@iconify-json/<set>` is a much smaller install.
232
+
233
+ ## `@tabler/icons` {#tabler-icons}
234
+
235
+ **Adapter — the [`iconSource` wrapper](#iconsource).** @tabler/icons@3.46.0.
236
+
237
+ 5,000+ MIT icons as SVG files under `icons/outline/` (stroke-based, 24×24)
238
+ and `icons/filled/`, with an `exports` map that resolves file paths, plus
239
+ `tabler-nodes-outline.json`/`-filled.json` — per-icon node arrays like
240
+ `lucide`'s data exports.
241
+
242
+ ```jsx
243
+ import fs from 'node:fs';
244
+ import { iconSource } from './icon-source.js';
245
+
246
+ const tabler = (name, style = 'outline') =>
247
+ fs.readFileSync(
248
+ new URL(import.meta.resolve(`@tabler/icons/${style}/${name}.svg`)),
249
+ 'utf8',
250
+ );
251
+
252
+ const Icon = ({ name, style, size = 24, color = '#000' }) => (
253
+ <svg
254
+ source={iconSource(tabler(name, style), color)}
255
+ style={{ width: size }}
256
+ />
257
+ );
258
+
259
+ // <Icon name="home" color="#dc2626" />
260
+ // <Icon name="settings" style="filled" size={16} />
261
+ ```
262
+
263
+ **Prefer `@iconify-json/tabler` + `@iconify/utils`**: identical art, no
264
+ adapter. Use this package when you want the files themselves or the JSON
265
+ node arrays.
266
+
267
+ - Outline icons start with a
268
+ `<path stroke="none" d="M0 0h24v24H0z" fill="none"/>` bounds rect, which
269
+ paints nothing. Harmless.
270
+ - `@tabler/icons-react` throws ([register](../ecosystem.md#render-time)).
271
+
272
+ ## `@phosphor-icons/core` {#phosphor-icons}
273
+
274
+ **Adapter — the [`iconSource` wrapper](#iconsource).**
275
+ @phosphor-icons/core@2.1.1.
276
+
277
+ Raw SVG files under `assets/<weight>/<name>.svg` for six weights (`thin`,
278
+ `light`, `regular`, `bold`, `fill`, `duotone`), 256×256 viewBox,
279
+ fill-based. Because the default fill matches, the raw file renders legible
280
+ black glyphs; the wrapper is only needed for colour. `duotone`'s per-path
281
+ opacity survives either way.
282
+
283
+ ```jsx
284
+ import fs from 'node:fs';
285
+ import { iconSource } from './icon-source.js';
286
+
287
+ const phosphor = (name, weight = 'regular') => {
288
+ const file = weight === 'regular' ? name : `${name}-${weight}`;
289
+ return fs.readFileSync(
290
+ new URL(
291
+ import.meta.resolve(`@phosphor-icons/core/assets/${weight}/${file}.svg`),
292
+ ),
293
+ 'utf8',
294
+ );
295
+ };
296
+
297
+ const Icon = ({ name, weight, size = 24, color = '#000' }) => (
298
+ <svg
299
+ source={iconSource(phosphor(name, weight), color)}
300
+ style={{ width: size }}
301
+ />
302
+ );
303
+
304
+ // <Icon name="house" color="#0ea5e9" />
305
+ // <Icon name="house" weight="fill" size={32} />
306
+ ```
307
+
308
+ - Non-`regular` weights use suffixed filenames
309
+ (`assets/bold/house-bold.svg`); the helper above handles it.
310
+ - `@iconify-json/ph` serves the same set with no adapter and no filesystem
311
+ access. Prefer it unless you want the files.
312
+ - `@phosphor-icons/react` throws ([register](../ecosystem.md#render-time)).
313
+
314
+ ## `heroicons` (the asset package) {#heroicons-assets}
315
+
316
+ **Adapter — the [`iconSource` wrapper](#iconsource).** heroicons@2.2.0.
317
+
318
+ The Tailwind set as plain SVG files, no JS: `24/outline/*.svg`
319
+ (stroke-based), `24/solid/`, `20/solid/` and `16/solid/`.
320
+
321
+ ```jsx
322
+ import fs from 'node:fs';
323
+ import { iconSource } from './icon-source.js';
324
+
325
+ const heroicon = (name, style = '24/outline') =>
326
+ fs.readFileSync(
327
+ new URL(import.meta.resolve(`heroicons/${style}/${name}.svg`)),
328
+ 'utf8',
329
+ );
330
+
331
+ const Icon = ({ name, style, size = 24, color = '#000' }) => (
332
+ <svg
333
+ source={iconSource(heroicon(name, style), color)}
334
+ style={{ width: size }}
335
+ />
336
+ );
337
+
338
+ // <Icon name="home" color="#f59e0b" />
339
+ // <Icon name="home" style="24/solid" color="#0ea5e9" />
340
+ ```
341
+
342
+ Use this rather than `@heroicons/react` when you need to recolour. There is
343
+ no JS index, so enumerate the directory for a picker. **Prefer
344
+ `@iconify-json/heroicons`** through the [Iconify](#iconify) pipeline — same
345
+ art, no adapter, no filesystem access — unless you want the files.
346
+
347
+ ## `feather-icons` {#feather-icons}
348
+
349
+ **Adapter, ~5 lines.** feather-icons@4.29.2.
350
+
351
+ The classic 24×24 stroke set, 287 icons. `.contents` is the inner SVG
352
+ markup without a root element, so the adapter is a template literal: build
353
+ the root and the `<g>` yourself and fact 1 never applies.
354
+
355
+ ```jsx
356
+ import feather from 'feather-icons';
357
+
358
+ function featherSvg(name, color = '#000', strokeWidth = 2) {
359
+ return `<svg viewBox="0 0 24 24"><g fill="none" stroke="${color}"
360
+ stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round"
361
+ >${feather.icons[name].contents}</g></svg>`;
362
+ }
363
+
364
+ const Icon = ({ name, size = 24, color, strokeWidth }) => (
365
+ <svg source={featherSvg(name, color, strokeWidth)} style={{ width: size }} />
366
+ );
367
+
368
+ // <Icon name="alert-circle" color="#f59e0b" />
369
+ ```
370
+
371
+ - **Prefer Lucide**, Feather's actively developed fork with five times the
372
+ coverage and the same look; Feather has had no new icons in years. Use it
373
+ only to match an existing design.
374
+ - `feather.icons.x.toSvg()` into `<svg source>` renders wrong: `toSvg()`
375
+ puts every paint attribute, and `toSvg({ color })`'s `color`, on the root,
376
+ where they are ignored. Always go through `.contents`.
377
+ - `feather.replace()` (the DOM `<i data-feather>` swapper) is browser-only.