Skip to main content

react-x11

A React renderer whose host environment is an X11 server — no DOM, no HTML, no native bridge

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.

What react-x11 is →

Browse the API reference →

Run it here — no X server needed →

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.

How that works, and how it is measured →

npm run bench

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.