Build desktop UIs with React
Write components, hooks and flexbox; get a window on a Linux desktop, on macOS under XQuartz, or on a display forwarded over ssh. Only <window>, <popup> and <glarea> are real X windows — everything else is a retained node painted into its window's double-buffered context, so a thousand-row table is a layout pass, not a thousand X resources.
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 />);
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 — 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.
Text is shaped once and its glyphs uploaded once, so drawing a line afterwards names them by index — about a byte per glyph. Gradients, scaling, alpha compositing and clipping are single server-side requests rather than loops over a pixel array, and nothing is read back. An update costs what the drawing costs, not what the window's area costs, which is why this stays comfortable on a display forwarded over ssh.
Because protocol cost is the design, it is measured rather than assumed. The benchmark reports requests, bytes, replies, RENDER composites and the pixel area those composites touch, against a checked-in baseline that a pull request has to update in the same diff.
That 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.
A renderer, not a wrapper
React computes what changed; react-x11 turns that into X11 protocol requests on a socket. No Electron, no browser engine, no WebView, no DOM, no native toolkit bridge — <div> is not an element that exists here.
Flexbox and inline styles
Layout is yoga, the engine React Native uses. Styles are inline objects with :hover, :focus, :active and :disabled blocks that repaint one node without a React render, plus '@width >= 600' size queries, theme tokens and transitions.
Widgets included
Button, Select, Slider, Switch, Dialog, MenuBar, Tabs, Tree, SplitPane and a virtualized Table — plain React over the primitives, themable, and nothing you could not have written yourself.
The tooling you already use
The standard React DevTools app, with highlight-on-hover into the X11 window. Fast Refresh hot reloading that keeps the connection, the window and your state. Alt+Click to open a component's source. TypeScript declarations in the box.
Some react-three-fiber
<mesh>, <group>, geometries, materials with textures, lights and raycast pointer events — drawn over indirect GLX, so the GL protocol travels the same X connection. Each geometry compiles to a server-side display list.
JavaScript all the way down
npm install never compiles anything, and npm test needs no X server: node-x11 ships a pure-JS X server that the tests render into and read pixels back from. Every screenshot on this site was made that way — so is the playground.
Rendered by the code it documents
Every shot below comes out of the repo's own examples, driven through the real event pipeline against an in-process X server and read back pixel by pixel.



