GPUIX is a React reconciler pointed at Zed's window system
Node.js & React bindings for Zed’s GPUI. Build memory efficient native apps with React and no Electron
At a glance
- What is it?
- GPUIX lets you write a React or Solid tree in TypeScript and have GPUI paint it, with no Electron and no web view. The interesting parts are the ones the README warns about: the adapter and native package have to be pinned to the same exact version, jsxImportSource is not optional, text with no colour renders black, and packaging does not turn on updates.
- Who is it for?
- Adopt GPUIX when you want React's component model without shipping a browser engine, and when your users are on one desktop platform at a time. Do not adopt it expecting version stability: the project says so itself, in bold, that it is still pre-1.0 and a new release can break either package.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 14 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- remorses/gpuix2,952 ★ · +466
| Repository | 2026-09-30 | 2026-11-14 |
|---|---|---|
| remorses/gpuix | 2.5k | 3k |
466 Stars in the last 45 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
React's reconciler with a different renderer behind it
The mechanism is visible in the manifest rather than the prose. `react-reconciler` is a direct dependency, which is the package React itself uses to talk to a host environment, and GPUIX points it at GPUI instead of at a browser DOM.
That is what makes the promise work. `useState` and JSX still apply, so the component model is React's, and the layout, text and input handling go through GPUI rather than the DOM. Rendering ends up on Metal, DirectX or Vulkan depending on the platform, with no Electron process and no web view anywhere in the stack.
The project's own framing is that everything visible in its screenshots is its own work rather than a browser's: the glass window, the cards, the schedule and the native text. The schedule is a good test of that claim, since scheduling implies text measurement and layout behaviour that a web view would normally hand you.
React and Solid are both supported through separate packages, and the dependency list pins React at a 19.x line with the reconciler at a 0.33 line, so the version pairing is part of the surface area a user inherits.
The adapter and the native package must be pinned together
This is the warning the README puts in bold, and it is the one to read before anything else.
The text says to pin your adapter and `@gpuix/native` to the same exact version, because GPUIX is still pre-1.0 and a new release can break either package. Then it explains the mechanism of the failure, which is the useful part: the adapter pulls `@gpuix/native` with a version range, and that range can install a newer native binary underneath an older adapter.
So the breakage is not a version conflict you would see in a lockfile warning. It is a compiled binary and a JavaScript adapter that were never tested together, discovered at runtime, on the user's machine. The fix is procedural, which is why the build from scratch instructions use an exact flag:
bun add --exact @gpuix/react @gpuix/native react
bun add -d @types/react typescriptBoth as direct dependencies, upgraded together. Nothing enforces this for you, which is the honest trade of a pre-1.0 project: the flexibility of independent releases in exchange for a version discipline the user has to maintain by hand.
jsxImportSource is not an optimisation, it is a requirement
The configuration step is short and it is marked as required, because without it TypeScript checks your components against the wrong set of types.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@gpuix/react",
"strict": true,
"skipLibCheck": true,
"noEmit": true
}
}Without `jsxImportSource` set to the React package, TypeScript falls back to DOM types. The README lists what then breaks: the `virtual-list`, `markdown` and `code` elements are unknown, and the `hover` key inside a style object is a type error. Those are the custom elements and the style properties that make a GPUI tree different from a web page, so this single setting is the line between the two type universes.
The rest of the configuration is conservative. `noEmit` is on, which tells you the TypeScript compiler is not the bundler here; `bun` is. `moduleResolution` is set to `bundler`, and `skipLibCheck` is on, so type errors inside dependencies are not reported.
Text with no color paints black and vanishes
There is one callout marked important in the quick start, and it is a rendering gotcha rather than a configuration note.
GPUI does not inherit `color` from a parent element. So a `text` element with no explicit color of its own paints black, and on the dark surface the rest of the example uses, the text disappears. The instruction is blunt: give every `text` a color.
This is the cost of the DOM being gone. CSS inheritance across a subtree is not free, and the example code makes the alternative visible, since every `text` element in it carries its own `color` in its style object.
The same style objects carry a `hover` key holding a second set of properties, which is how a hover state is expressed without a pseudo selector. Those are the two places where GPUX's style object stops looking like React's inline style, and both are exactly what the TypeScript configuration in the previous section is there to type.
One more thing about that example. The entry file is supposed to end with a `render` call that creates the window, mounts React and starts the frame loop, and the printed example stops in the middle of that call. The final argument is not shown, so anyone copying the snippet has to find the options shape elsewhere.
bun --hot or you open a second window
The quick start is three commands.
bunx @gpuix/cli new my-app
cd my-app
bun run devWhat makes this cheap is what the command does not do. It downloads only the `example-app/` directory and installs its dependencies, so there is no repository clone, no native build and no Rust toolchain in the path, even though the renderer underneath is native. Installing `@gpuix/react` pulls the native renderer for your platform.
Then the development loop. `@gpuix/react` gives you a hot remount: edit `app.tsx` and the running window remounts on save, and click and keyboard handlers switch to the new tree without recreating the window.
The command that does that is not the obvious one:
bun --hot app.tsxThe README says to use `bun --hot` and not plain `bun`, because plain `bun` starts a second window rather than remounting in the first one. For a windowed native application that difference is the whole ergonomics of the tool, and it is the kind of detail that gets discovered the hard way.
82 MB with Bun, 34 MB with Hermes
Shipping is one command, and the output is self contained.
bun build --compile app.tsx --outfile dist/app
./dist/appThe compiled binary carries the renderer, so the resulting application runs with no Bun install and no Node install on the target machine. That is the payoff of not shipping a browser.
The size is the cost. The README reports the Bun chat application bundle at 82 MB on the machine it was measured on. The alternative is to run the same React application on a Node runtime called hermes-node instead of Bun, which the documentation describes as 12 MB plus a 22 MB native sidecar. Those two numbers are also the reason the smaller Hermes chat application bundle is reported at 34 MB.
On this machine, with no runtime installed on the target, is the measurement caveat that belongs in any comparison.
A raw binary has no Dock icon, so the documented next step wraps it with cargo-packager, installed with a locked cargo install. The icon needs one specific input: build an `.icns` from a 1024 PNG and pass the `.icns`, because cargo-packager rejected the raw 1024 PNG with the message `No matching IconType`. Output formats are per host operating system, an app bundle and optionally a disk image on macOS, an installer executable on Windows and an AppImage on Linux.
Packaging does not enable updates, and the signature is a sibling file
Two separate systems, and the README is careful to keep them apart.
Packaging does not turn on updates. The running application calls `checkUpdate` on `@gpuix/native`, and the note says HTTP uses the same client as the rest of the native code, so there is no second native addon to ship. One sentence in that paragraph is broken, with an empty code span where a library name should be, which tells you the paragraph was edited at speed.
The update flow has a fixed shape. Host the build on GitHub Releases, create the release first, and let CI pack on each operating system, sign, and upload the bundle together with its `.sig` file. The application requests the `releases/latest` URL for your repository, which redirects to the API form, and the updater reads `tag_name` and `assets` before fetching the sibling file named after each asset.
Signing is generated once with `cargo packager signer generate`. The private key and its password go into repository secrets under two named variables, and only the public key goes into the application, where it is passed to `checkUpdate` alongside the endpoint and the current version. Signing is automatic when those two variables are set.
Two details close the section. The packager only builds the host operating system, so it runs on whichever of macOS, Linux or Windows you invoke it on. And `--release` is the packager profile rather than a cargo build flag, which is the kind of collision that produces a wrong binary if you assume otherwise.
One repository, three package names, two agent instruction files
The root listing explains how the project is organised to release.
The manifest declares five workspaces: `cli`, `packages/*`, `examples`, `example-app` and `disktree`. The build script runs the native, React and Solid builds in sequence, and the dev script is a TypeScript file rather than an inline command. Releases confirm the split, because the three newest tags are three different package names, `[email protected]` on 2026-09-27, `@gpuix/[email protected]` on 2026-09-23 and `@gpuix/[email protected]` on 2026-09-21. All are pre-1.0, which is the version state the pinning warning refers to.
Two version-control details matter for a contributor. `.changeset/` holds the changeset entries that drive those releases, and `.gitmodules` is present because the `zed` directory is the GPUI source rather than a copy of it. A `rust-toolchain.toml` pins the Rust side, and `bun.lock` the JavaScript side.
Then there are the files that have nothing to do with the product: `AGENTS.md` and `CLAUDE.md` at the root, a `plans/` directory, a `website/` directory whose guides include the Hermes page the README points at, and `THIRD_PARTY_NOTICES.md` next to the Apache-2.0 license.
Testing runs on vitest, and the examples directory carries both ordinary and performance variants of the same components, so the chat and timeline examples exist partly as benchmarks. The last push was on 2026-09-27 and the repository is not archived.
Editorial conclusion
Adopt GPUIX when you want React's component model without shipping a browser engine, and when your users are on one desktop platform at a time. Do not adopt it expecting version stability: the project says so itself, in bold, that it is still pre-1.0 and a new release can break either package. Three things to check before the first commit: pin the adapter and the native package to the same exact version, because the range in the adapter's dependency is exactly how an older adapter ends up running a newer native binary; set jsxImportSource in the TypeScript configuration or nothing typechecks; and decide separately how you will ship updates, since packaging and updating are two different systems and the updater expects a GitHub release with a sibling signature file.
Frequently asked questions
What is GPUIX?
React and Solid bindings for GPUI, Zed's GPU UI framework. You write a component tree in TypeScript and GPUIX paints it with Metal, DirectX or Vulkan, with no Electron and no web view, while useState and JSX keep working as they do in React.
How do I start a new GPUIX app?
Run bunx @gpuix/cli new my-app, then cd my-app and bun run dev. That command downloads only the example-app directory and installs its dependencies, so there is no repository clone, no native build and no Rust toolchain, and installing @gpuix/react pulls the native renderer for your platform.
Why does GPUIX require jsxImportSource?
Without it TypeScript checks your components against DOM types, and elements such as virtual-list, markdown and code plus the hover key in a style object all fail to typecheck. The documented tsconfig sets jsxImportSource to @gpuix/react alongside jsx react-jsx, strict, noEmit and moduleResolution bundler.
Why must the GPUIX adapter and native versions match?
GPUIX is still pre-1.0 and a new release can break either package. The adapter pulls @gpuix/native with a version range, and that range can install a newer native binary under an older adapter, so the README says to add both as direct dependencies with exact versions and upgrade them together.
How large is a GPUIX app bundle?
The README reports 82 MB for the Bun chat bundle and 34 MB for the Hermes one, where the Hermes path is described as 12 MB plus a 22 MB native sidecar. A Bun compiled binary carries the renderer and runs with no Bun and no Node installed; cargo-packager wraps it per host OS, app on macOS, nsis on Windows, appimage on Linux.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/remorses-gpuix)Star history chart for your README
[](https://hysenlabs.com/projects/remorses-gpuix)Was this useful?