Skip to main content

Ecosystem

react-x11 is a React renderer with no DOM: no document, no window global, no CSSOM, and the host elements are <box>/<text>/<svg>, not <div>/<span>. A package that only touches React — hooks, context, state, reconciliation — works unchanged; one that reaches for document, window, HTMLElement, CSS, or renders a <div> does not.

That rule decides every verdict below. Each was established by running the library against react-x11 1.2.0, not by reading its documentation; React DevTools and Fast Refresh, which react-x11 wires up itself, are documented as working setups instead.

The 30-second test​

Before installing anything, ask four questions in order. Each has a command.

  1. Does it import? node --input-type=module -e "import('the-package')". A package that hard-requires react-dom, or whose exports map has only browser conditions, fails here (import time). If something else in your tree already pulled in react-dom, this passes and the problem moves to the silent column — run npm ls react-dom before you trust a pass.

  2. Does it render host elements? Grep the shipped code for tag names:

    grep -rEoh '"(div|span|button|input|form|style|a)"' \
    node_modules/the-package --include='*.js' --include='*.mjs' | sort -u

    Anything that emits DOM tags cannot render here, however headless the README says it is. This one is a reliable verdict: zustand and @tanstack/react-table print nothing; recharts, react-select and Headless UI print a list.

  3. Does it read DOM globals? Same shape, different pattern:

    grep -rnE 'typeof window|typeof document|HTMLElement|getBoundingClientRect' \
    node_modules/the-package --include='*.js' --include='*.mjs'

    A pointer, not a verdict — read the hits. A minified UMD bundle or an isSSR = typeof window === "undefined" default matches harmlessly. Look for an environment probe whose result gates a feature: that is the silent-failure shape, where the library decides it is doing SSR and quietly turns things off. Check the transitive core too — @tanstack/query-core, not just @tanstack/react-query, which has no hits of its own.

  4. Does it expect DOM-shaped events? Usually fine: <textinput onChange> passes a synthetic event with target.value (writable) and target.name, so event.target.value reads as it does in the DOM. Check the opposite direction: a library whose field contract is value-in/value-out (field.handleChange(value)) needs one unwrap per field, and a bare onChange={field.handleChange} stores the event object with no error. Grep your own code for onChange={ on a <textinput>.

A library that passes all four is almost always a drop-in. One that fails 2 or 3 usually has a headless core published separately — the pattern behind half the adapters on this page.

What works​

Fifty-five packages and two Node built-ins, across 12 categories. Out of the box means install and use; adapter means a little glue, written out in full on the category page.

State management​

PackageIntegration
zustandout of the boxstate
jotaiout of the boxstate
valtioout of the boxstate
xstate + @xstate/reactout of the boxstate
redux + @reduxjs/toolkit + react-reduxout of the box — pin react-redux ≥ 9state
immerout of the boxstate
mobx + mobx-react-liteadapter — no code, but react-dom must be installedstate

Data fetching​

PackageIntegration
@tanstack/react-queryadapter, ~12 linesdata fetching
swradapter, 1 linedata fetching

Forms and validation​

PackageIntegration
@tanstack/react-formout of the boxforms
zodout of the boxforms
react-hook-formout of the box — register() or <Controller>forms
formikout of the box — no <Form>; <Field> with a render propforms
downshiftout of the box — getInputProps() on a <textinput>forms

Icons​

PackageIntegration
@iconify/utils + @iconify-json/*out of the boxicons
@heroicons/reactout of the box — size it with styleicons
lucideadapter, ~10 linesicons
lucide-staticadapter, ~12 lines (iconSource)icons
react-iconsadapter, ~15 linesicons
@tabler/iconsadapter — iconSourceicons
@phosphor-icons/coreadapter — iconSourceicons
heroicons (the asset package)adapter — iconSourceicons
feather-iconsadapter, ~5 linesicons

Theming, palettes and colour​

PackageIntegration
@radix-ui/colorsout of the boxtheming
@catppuccin/paletteout of the boxtheming
@material/material-color-utilitiesout of the box from a source colourtheming
colordout of the boxtheming
culoriout of the boxtheming
open-propsout of the box for colourstheming
tailwindcss (/colors export only)adapter, ~3 lines — oklch → hextheming
daltonizeadapter, ~4 linestheming

Animation​

PackageIntegration
popmotionout of the boxanimation
d3-easeout of the boxanimation
d3-interpolateout of the boxanimation
@tweenjs/tween.jsout of the boxanimation
flubberout of the boxanimation
react-transition-groupout of the box — with nodeRefanimation
@react-spring/core + /animatedadapter, ~20 lines (createHost)animation

Headless components​

PackageIntegration
react-statelyout of the boxheadless
@tanstack/react-tableout of the boxheadless

Layout and positioning​

PackageIntegration
@floating-ui/coreadapter, ~35 lines (platform object)layout
@tanstack/virtual-coreadapter, ~40 lines — the core, not the React wrapperlayout

Routing​

PackageIntegration
wouterout of the box — wouter/memory-locationrouting
react-routerout of the box — MemoryRouter, no <Link>routing

Internationalization​

PackageIntegration
i18next + react-i18nextout of the boxi18n

Testing​

PackageIntegration
node:test (built-in)out of the boxtesting
vitestout of the boxtesting
mswout of the box — msw/nodetesting
fast-checkout of the boxtesting
sinonout of the box — one required toFake restrictiontesting
pixelmatchout of the boxtesting
react-test-rendererout of the box — but deprecated upstreamtesting

Developer tooling​

PackageIntegration
react-devtools-coreout of the box — already wired behind REACT_X11_DEVTOOLSdev tooling
react-refresh + hot-module-replacementsupported — the react-x11/refresh entry pointdev tooling
node --inspect (built-in)out of the boxdev tooling
0xout of the boxdev tooling
@welldone-software/why-did-you-renderout of the boxdev tooling

What does not work​

Everything below was reproduced against react-x11 1.2.0, and the error text is verbatim — if you searched for one of these strings, this is the right page. Failures come in four flavours, by cost:

  • Install and import time — loud, immediate, cheap.
  • Render time — loud, immediate; the message usually names the offending element.
  • First interaction — the tree mounts clean and breaks on the first keystroke, with a stack trace that points at the library, not the renderer.
  • Silent — no error, ever. By far the most expensive; read that table even if nothing is broken yet.

Install and import time​

PackageErrorUse instead
react-redux@8npm error code ERESOLVE … peer react@"^16.8 || ^17.0 || ^18.0" from react-redux@8.1.3react-redux@9 — state
react-redux@8 (past ERESOLVE)Error: Cannot find module 'react-dom' from react-redux/lib/utils/reactBatchedUpdates.jsreact-redux@9
@tanstack/react-virtualError [ERR_MODULE_NOT_FOUND]: Cannot find package 'react-dom' imported from node_modules/@tanstack/react-virtual/dist/esm/index.js@tanstack/virtual-core — layout
styled-componentsTypeError: styled.div is not a functioncreateStyles/ThemeProvider + a palette package — theming
react-scanError [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './auto' is not defined by "exports" in node_modules/react-scan/package.json imported from …React's own <Profiler>, or why-did-you-render — dev tooling
@testing-library/react (no react-dom)Error: Cannot find module 'react-dom/test-utils'react-x11/test on node:test or vitest — testing
jest (no ESM config)Jest encountered an unexpected token … SyntaxError: Cannot use import statement outside a modulenode:test/vitest, or Jest in ESM mode — testing
jest (transformed to CJS)SyntaxError: Cannot use 'import.meta' outside a moduleas above

The styled-components error is misleading: it fires at module evaluation, mentions neither react-x11 nor the DOM, and looks like a version mismatch.

import styled from 'styled-components';
const Panel = styled.div`
background: #1e1e2e;
`; // throws right here

Under plain Node ESM (how react-x11 apps run, with no bundler) the default import resolves to the CJS module namespace — an object with ServerStyleSheet and friends, and no .div. Fixing the interop with const styled = sc.default ?? sc only moves you to react-x11: unknown element type <div>; styled('box') is in silent failures.

The two Jest entries are the same dead end from two directions. Full text:

Jest encountered an unexpected token
…
node_modules/react-x11/src/Reconciler.js:27
const _require = (0, _nodeModule.createRequire)(import.meta.url);
^^^^
SyntaxError: Cannot use 'import.meta' outside a module

react-x11 calls createRequire(import.meta.url), which cannot be down-compiled to CJS, so Jest runs react-x11 tests only in real ESM mode — see testing.

Render time​

The renderer's own errors are prefixed react-x11: and name the element it could not create. By far the most common failure; the tag tells you which library layer broke.

PackageErrorUse instead
framer-motionreact-x11: uncaught error Error: react-x11: unknown element type <div>. Supported: <window>, <popup>, <box>, <text>, <image>, <canvas>, <textinput>, <textarea>, <svg>, <glarea>.popmotion, @tweenjs/tween.js, d3-ease + d3-interpolate — animation
framer-motion (value layer)ReferenceError: HTMLElement is not definedpopmotion — animation
rechartsreact-x11: uncaught error Error: react-x11: unknown element type <div>. …d3-scale + d3-shape into the <svg> host — below
formik (<Form> with a <Field>)react-x11: uncaught error Error: react-x11: unknown element type <input>. …a <Field> render prop onto a <textinput> — forms
formik (<Form> alone)react-x11: uncaught error Error: react-x11: unknown element type <form>. …no <Form>; call submitForm() from a Button
@headlessui/react (Switch)react-x11: uncaught error Error: react-x11: unknown element type <button>. …react-x11's built-in widgets — components
@headlessui/react (Dialog)react-x11: uncaught error Error: react-x11: unknown element type <span>. …react-x11's built-in Dialog
@radix-ui/react-dialogreact-x11: uncaught error Error: react-x11: unknown element type <button>. …react-x11's built-in Dialog
react-selectreact-x11: uncaught error Error: react-x11: unknown element type <style>. …react-x11's built-in Select
@emotion/styledreact-x11: uncaught error Error: react-x11: unknown element type <style>. …createStyles + ThemeProvider — styling
react-dropzonereact-x11: uncaught error Error: react-x11: unknown element type <input>. …built-in useDropTarget / <box dropAccept onDrop> — drag and drop
react-router-dom (BrowserRouter)react-x11: uncaught error ReferenceError: document is not defined at getUrlBasedHistoryMemoryRouter — routing
react-router-dom (<Link>)react-x11: uncaught error Error: react-x11: unknown element type <a>. …useNavigate() on a <box onClick>
lucide-reactreact-x11: uncaught error Error: react-x11: <svg width=…> is a style property — pass it in style: <svg style={{ width: … }} />@iconify-json/lucide, or lucide — icons
@tabler/icons-reactreact-x11: uncaught error Error: react-x11: <svg width=…> is a style property — pass it in style: <svg style={{ width: … }} />@iconify-json/tabler, or @tabler/icons — icons
@phosphor-icons/reactreact-x11: uncaught error Error: react-x11: <svg width=…> is a style property — pass it in style: <svg style={{ width: … }} />@iconify-json/ph, or @phosphor-icons/core — icons
@testing-library/reactReferenceError: document is not defined at render (node_modules/@testing-library/react/dist/pure.js:256:5)renderX11 from react-x11/test — testing
@testing-library/dom (within)TypeError: Expected container to be an Element, a Document or a DocumentFragment but got WindowNode.within from react-x11/test — testing
@testing-library/dom (screen)TypeError: For queries bound to document.body a global document has to be available... Learn more: https://testing-library.com/s/screen-global-errorscreen from react-x11/test — testing
@testing-library/user-eventTypeError: Cannot read properties of undefined (reading 'Symbol(Node prepared with document state workarounds)')userEvent from react-x11/test, or raw ntk events — testing
jest-image-snapshotTypeError: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received an instance of ObjectgetImageData + pngjs + pixelmatch — testing
react-x11 test selectorsError: Test selector API is not supported by this renderer.nothing — supportsTestSelectors is false

Four of those need detail, because the obvious workaround fails.

framer-motion cannot be rescued by dropping to its value layer. Reaching past motion.div fails too:

import { animate, motionValue } from 'framer-motion';
const mv = motionValue(0);
mv.on('change', (v) => console.log(v));
animate(mv, 100, { duration: 0.1 });
// ReferenceError: HTMLElement is not defined

motion-dom feature-detects the Web Animations API with subject instanceof HTMLElement, so animate() on a plain motionValue still throws, asynchronously, once keyframes resolve (from motion-dom/src/animation/waapi/supports/waapi.ts). Use popmotion's animate() instead.

The Radix escape hatch is worse than the error. <Dialog.Portal><Dialog.Content /></Dialog.Portal> produces no error and no output: Portal is SSR-guarded, finds no document, and renders nothing. Every Radix primitive has that shape. (@radix-ui/colors is pure data and recommended.)

react-dropzone fails twice. Deleting the <input> does not rescue it: with only <box {...getRootProps()} /> you get past the reconciler and then hit react-x11: uncaught error ReferenceError: window is not defined, thrown from react-dropzone's own mount effect.

The three *-react icon packages produce a byte-identical error, not "unknown element type": react-x11 has a real <svg> host, so the icon gets further and dies on the flat width/height props those wrappers always set. No prop avoids it. @heroicons/react is the exception: it sizes with a class name, so it renders.

Index notes for searchers:

  • react-select reports <style>, not <div>: it is built on Emotion, and the first host it emits is Emotion's injected style tag.
  • formik reports <input> when your <Form> has a <Field> in it and <form> when it does not, because the reconciler completes children first.
  • react-router-dom's <Link> with a bare string child hits the raw-text error first: react-x11: raw text "go" must be wrapped in a element (or be the string child of an SVG).

Charting, without recharts​

The substitute for recharts is small: compute with d3-scale + d3-shape and emit the path into react-x11's own <svg> host.

import { scaleLinear } from 'd3-scale';
import { line } from 'd3-shape';

const data = [
[0, 3],
[1, 7],
[2, 4],
[3, 9],
[4, 6],
];
const x = scaleLinear().domain([0, 4]).range([0, 200]);
const y = scaleLinear().domain([0, 10]).range([100, 0]);
const d = line()
.x((p) => x(p[0]))
.y((p) => y(p[1]))(data);

<svg style={{ width: 200, height: 100 }} viewBox="0 0 200 100">
<path d={d} stroke="#89b4fa" fill="none" strokeWidth={2} />
</svg>;

The <canvas> host with an onDraw handler is the other option, and the better one for dense plots.

First interaction​

These mount without an error; the first sign of trouble is a keystroke.

Nothing currently fails here. downshift (a TypeError on the first keystroke) and formik's getFieldProps spread (the field silently never updated) did on 1.x, when <textinput> called onChange(newString); it now passes a synthetic event and both work (forms). The two shapes this class of bug takes:

  • A handler that reads event.nativeEvent.<something> unguarded works for a keystroke and throws for a paste or an undo, where there is no X event to report. downshift reads event.nativeEvent.preventDownshiftDefault this way, and is safe only because it checks hasOwnProperty('nativeEvent') first and the property is always present.
  • A library that writes through a ref — ref.value = '' — needs the node to have a value setter. <textinput> has one; most nodes do not, and the TypeError is thrown inside the commit phase, taking the render down rather than one handler.

Silent failures​

No error is ever printed. Read this table before you spend an afternoon.

PackageWhat happensFix
swr(silent) data never refetches when the X11 window regains focus — no error, no warning, revalidateOnFocus just never firesdefine globalThis.window before importing swr, or use @tanstack/react-query — data fetching
@tanstack/react-virtual (with react-dom installed)(silent) with react-dom installed the import resolves and the list renders, but the flushSync re-measure applies to react-dom's reconciler, not react-x11's@tanstack/virtual-core — layout
react-scan(silent) no error, no output — onRender never fires and getReport() stays emptyReact's own <Profiler> (verified working), or why-did-you-render
@radix-ui/react-dialog (Dialog.Portal)renders nothing; the SSR guard finds no documentreact-x11's built-in Dialog
styled-components (styled('box'))renders with no error and hands the box a single className prop, which react-x11 ignores — the CSS goes nowherecreateStyles + ThemeProvider
tailwindcss/colors (raw values)v4 ships oklch() strings; ntk's parser returns null and paints nothingconvert with culori — theming

The swr one is the most expensive. useSWR fetches, caches and dedupes, but swr decides once at import that a process with no global window is a server and registers no focus or reconnect listeners — not even a custom initFocus passed through <SWRConfig value={{ provider, initFocus }}>, the documented React Native escape hatch. Details and the workaround: data fetching.

@tanstack/react-virtual is subtle differently. react-dom is a required peer, so a default npm i @tanstack/react-virtual installs it and the import-time error never appears; react-dom@19's flushSync() outside any react-dom root runs the callback without throwing, so nothing warns you that the virtualizer's synchronous flush happens in the wrong renderer.

Adding a library that is not listed​

Run the 30-second test first; it settles most cases without an install. If it passes, render it:

// probe.jsx — needs a display: run under $DISPLAY, or `xvfb-run -a`
import { createRoot } from 'react-x11';
import { TheThing } from 'the-package';

(await createRoot()).render(
<window width={300} height={200} title="probe">
<box style={{ flexGrow: 1 }}>
<TheThing />
</box>
</window>,
);

Four outcomes:

  • react-x11: unknown element type <…> — the library renders DOM hosts. Look for a headless core published as a separate package; that is what rescued TanStack Virtual, Floating UI and react-spring.
  • react-x11: <… width=…> is a style property — it reached a real react-x11 host and passed a flat style prop. Usually fixable by wrapping the component rather than replacing it.
  • ReferenceError: document is not defined (or window, or HTMLElement) — a DOM global at module or mount time. Look for a documented environment-override seam, like @tanstack/react-query's environmentManager.
  • It renders. Then type into it, click it, and resize the window; the event-shape and silent classes only show up here.

If you establish a result either way, open an issue on the react-x11 repository with the package name, its exact version, the code you ran, and the verbatim output. Negative results are as useful as positive ones.