What react-x11 is
react-x11 is a React renderer whose host environment is an X11 server.
There is no DOM and no HTML anywhere in it, and no browser engine underneath
it — this is not Electron with a different skin. React's job in a renderer is
to compute what changed; the renderer's job is to turn that into side effects
on some host. In react-dom those side effects are DOM mutations. Here they
are X11 protocol requests — CreateWindow, MapWindow,
ConfigureWindow, RenderCompositeGlyphs — written to a socket. <box> is
not a <div> with different paint; it is a retained layout node the renderer
draws with, and <div> is not an element that exists.
import React, { useState } from 'react';
import { createRoot } from 'react-x11';
function Counter() {
const [n, setN] = useState(0);
return (
<window width={240} height={120} title="counter">
<box
style={{
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 10,
}}
>
<text style={{ fontSize: 24 }}>{String(n)}</text>
<box
style={{
backgroundColor: '#2980b9',
borderRadius: 6,
padding: 8,
cursor: 'pointer',
':hover': { backgroundColor: '#1f6693' },
}}
onClick={() => setN(n + 1)}
>
<text style={{ color: 'white' }}>+1</text>
</box>
</box>
</window>
);
}
const root = await createRoot(); // connects via $DISPLAY
root.render(<Counter />);
That is a real X11 client. It opens a window on a Linux desktop, on macOS under XQuartz, or on a display forwarded over ssh. You can also run it right now in this browser — the playground boots a pure-JavaScript X server on the page and connects to it.
Everything is JavaScript
npm install never compiles anything, and there is no native bridge to a
toolkit:
| layer | what it does |
|---|---|
| react-x11 | React primitives, layout, painting policy — decides how often anything is drawn |
| ntk | a canvas-like 2d context, text layout, widgets — owns how drawing is encoded |
| node-x11 | the X11 protocol itself, in pure JS — think xlib rewritten in node |
Layout is yoga-layout (WASM), text shaping is fontkit, and drawing goes through the X RENDER extension — so composition, gradients and glyph drawing happen in the X server, and a line of text costs about a byte per glyph on the wire.
The wire carries drawing, not pixels
Every renderer has to decide what to send. react-x11 does not rasterize a frame on the client and ship the buffer across: React reconciles the component tree, the renderer turns that diff into drawing operations — filled and rounded rectangles, composited gradients, clip regions, runs of glyph indices — and the X server executes them. The server owns the pixels; the client never had them.
That is what X's RENDER extension is for, and using it well is most of what this layer does:
- Glyphs are uploaded once. Text is shaped by fontkit, the glyph images go into a server-side glyph set, and every subsequent draw names them by index — roughly a byte per glyph, whatever the point size.
- Gradients, scaling, alpha compositing and clipping are server-side operations, single requests rather than loops over a pixel array.
- The client does not read pixels back. A request that waits for a reply stalls the pipeline, so what the server already told us — atoms, geometry, glyph pages — is cached instead of asked for again.
The consequence is that an update's cost tracks the drawing it implies, not the window's area. Repaints are coalesced onto ntk's frame clock and bounded to the region that changed — a short list of rectangles, so two changes at opposite corners of the window do not drag everything between them along — and any subtree that does not reach into that region is skipped before a single request goes out. A layout change is still a full repaint, because a node that moved leaves stale pixels where it used to be; even then a whole-window repaint of a real UI is a few dozen batched operations, not a framebuffer, which is why a react-x11 program stays comfortable on a display forwarded over ssh, where shipping frames would not be.
Because that is the design, it is also measured rather than assumed:
npm run bench reports requests, bytes, replies, RENDER composites and
the pixel area those composites touch, against a checked-in baseline. The
last metric exists because the others hide the most common regression — a
change that adds almost nothing to the wire while multiplying the server's
work.
What maps to a real X window
Almost nothing, on purpose. Only <window>, <popup> and <glarea> are
real X11 windows. Everything else is a retained lightweight node — one yoga
node each — painted into the owning window's double-buffered 2d context on
ntk's frame clock, with synthetic events dispatched by front-to-back hit
testing.
That is what makes a thousand-row table cheap: a thousand X windows would be a thousand server-side resources and a thousand expose events, while a thousand drawn nodes are a layout pass and one repaint.
X windows are also created top-down in the commit phase, never in the render phase — React may discard a render pass, and a discarded pass must not have opened windows on your screen.
The feature set
- Elements —
<window>,<popup>,<box>,<scrollview>,<text>,<textinput>,<textarea>,<image>,<canvas>, plus rich-content wrappers<markdown>,<html>,<svg>and<tex>around ntk's document widgets. - Widget components —
Button,Checkbox,Radio,Switch,Slider,ProgressBar,Select,Tooltip,Dialog,MenuBar/ContextMenu,Tabs,Tree,SplitPaneand a virtualizedTable. Plain React over the primitives — no reconciler support, nothing you could not have written yourself. - Flexbox layout — the same yoga engine React Native lays out with, so
flexDirection,gap,paddingand friends behave the way you expect. - Inline styles with pseudo-states —
:hover,:focus,:active,:disabledin thestyleobject. They resolve in the renderer as a repaint of one node, with no React render, because each is something the node already knows about itself. - Window size queries —
'@width >= 600', the X11 analogue of@media: a style asks about the window it is laid out in, not the screen. - Theme tokens and transitions —
backgroundColor: '$panel'resolves against the nearestthemeabove the node, so a hoisted style can still follow the theme;transition: 120lerps numbers and colours on the window's frame clock. - Synthetic events — capture and bubble
phases,
onClickwith DOM-styledetailclick counting, hover enter/leave, wheel, keyboard, focus and Tab traversal,preventDefault()over element default actions, pointer capture, acursorstyle property. - A subset of react-three-fiber
—
<mesh>,<group>, box/plane/sphere/cylinder/torus geometries, materials with textures, four light types and raycast pointer events, drawn over indirect GLX: the GL protocol goes over the same X connection. No GPU bindings, no native module. - Hot reloading — React Fast Refresh under ESM loader hooks. Edit a component while the program runs and it updates in place: the X connection, the window and component state all survive.
- React DevTools — the standard standalone app. Component tree, props and hooks, and hovering a component in the tree tints its rect in the X11 window.
- TypeScript — declarations ship with the
package. Set
jsxImportSource: "react-x11"and the X11 elements type check — which is also what makes<div>a compile error rather than a runtime throw. - Click to component — Alt+Click a rendered element to open the JSX line that created it in your editor.
Things that are unusual about it
- Your tests do not need an X server. node-x11 ships a pure-JS X server; react-x11's own test suite renders into it and reads pixels back. So does every screenshot in these docs, and so does this site's playground.
- A
refis an escape hatch to the layer below. For drawn elements you get the retained node (its absolute rect,scrollTo, …); for<window>and<popup>you get the live ntk window, and the whole ntk API with it. - It can be the window manager. The repo's
examples/wm.jsxis a real reparenting WM whose frames — titlebar, buttons, eight resize handles — are react-x11 components, with foreign X windows reparented inside them. - Protocol cost is a design concern.
npm run benchmeasures requests, bytes, replies, RENDER composites and the pixel area those composites touch, against a checked-in baseline.
Where to go next
- Getting started — install it and run something.
- Playground — edit react-x11 and watch it render, with no X server at all.
- API reference — elements, styling, components, events, types.