debloatui
v0.1.0
Published
Fast, debloated native UI toolkit (raylib + raygui) with JavaScript app logic
Maintainers
Readme
debloatui
Native UI for JavaScript/TypeScript — without embedding a browser.
About
debloatui is a desktop UI toolkit for Node.js aimed at low-end machines and simple tools. It pairs a thin native renderer (raylib + raygui) with a JavaScript library for reactive UI and layout, so application logic stays in JS/TS while drawing stays native and lightweight.
Built on raylib and raygui
- raylib is a lightweight, modern, open-source C library for games and graphics. It is best known for a simple OpenGL pipeline; depending on platform and configuration, the raylib ecosystem also covers other backends (including paths used for Vulkan and software rendering). debloatui currently builds the standard desktop raylib backend (OpenGL).
- raygui sits on top of raylib and provides an immediate-mode library for native user interfaces — buttons, panels, sliders, scroll views, and more — without a retained widget tree or a full browser engine.
debloatui wraps that stack in an N-API native addon so Node can own app logic while the OS window and controls stay native.
JavaScript toolkit
On top of the native addon, debloatui provides a JavaScript/TypeScript library so you do not have to wire every control by hand each frame:
- Reactive UI (
require('debloatui/reactive')) — single-window apps with a global state object, a pureview(state)that returns control nodes, anddispatch(action)for updates. Control events (onclick,oninput,onchange, and so on) drive state transitions, while raygui still draws natively each frame. - Layout tools (
require('debloatui/layout')) — helpers such asgridLayoutContainerandgridSizethat assignx/y/w/hto children (columns, rows, spans, margins, padding) so you can build structured screens without manual pixel arithmetic.
You can still use the low-level API (run(options, frameCallback) and ui.button(...)) when you want full control. The reactive and layout packages are the recommended path for application UI.
Why not a WebView?
WebView-based shells work well for many products, but they are often overkill: large install size, high memory use, and heavy CPU/GPU cost for interfaces that are basically forms and tools. Shipping an entire browser to draw a desktop settings window is a poor fit when you only need native controls.
See Performance measurement for measured RSS/CPU on sample apps and a comparison with WebView-style stacks.
debloatui is an alternative when you want:
- Native drawing (raylib + raygui), not HTML/CSS/DOM
- Low bloat — no Chromium
- Reactive JS UI and layout helpers — ship screens quickly without a DOM
- JS/TS app logic for iteration speed
- Desktop as the primary target today (Linux, Windows, macOS)
Android support is planned and coming soon.
Architecture
┌──────────────────────────────────────────────┐
│ JavaScript / TypeScript │
│ state, IO, business logic │
│ frame / view → controls │
└─────────────────────┬────────────────────────┘
│ N-API (in-process)
┌─────────────────────▼────────────────────────┐
│ Native addon — owns window + render loop │
│ raylib (window / OpenGL) + raygui (UI) │
└──────────────────────────────────────────────┘| Layer | Responsibility | |-------|----------------| | Native | Window, graphics backend, frame timing, raygui controls, textures | | JavaScript | App state, events, files/network, product logic | | Bridge | One JS entry per frame; UI methods are thin N-API calls |
The native side owns the main loop; JS runs inside each frame. This is not FFI over a free-running Node event loop.
Quick start
Dependencies
Linux (Debian/Ubuntu):
sudo apt install build-essential cmake libgl1-mesa-dev \
libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-devmacOS: Xcode CLT + brew install cmake
Windows: Visual Studio Build Tools (C++) + CMake
Node: 18+
Install and run examples
npm install # builds the native addon (cmake-js + raylib/raygui)
npm run example:basic # low-level frame callback
npm run example:counter
npm run example:reactive # reactive state + view
npm run example:scroll # vertical GuiScrollPanel
npm run example:image # PNG textures
npm run example:layout # grid layout helper
npm run example:layout-ts # same UI in TypeScript (requires Bun)
# or: bun examples/ts/layout.ts
# or: npx --yes tsx examples/ts/layout.ts (see examples/ts/README.md)Usage
Low-level (immediate-mode frame callback)
const { run } = require('debloatui');
let show = false;
run({ title: 'My App', width: 400, height: 200 }, (ui) => {
if (ui.button(24, 24, 120, 30, 'Hello')) show = true;
if (show) {
const btn = ui.messageBox(85, 70, 250, 100, 'Hi', 'From JS state', 'OK;Cancel');
if (btn >= 0) show = false;
}
});Reactive layer (recommended)
Single window, global state, pure view(state), and dispatch(action):
const {
app, state, h, button, label,
} = require('debloatui/reactive');
const Increment = (s) => ({ ...s, count: s.count + 1 });
app({
init: { count: 0 },
window: { title: 'My App', width: 400, height: 200 },
view: (s) => h(
label({ x: 24, y: 24, w: 200, h: 24, text: `count = ${s.count}` }),
button({ x: 24, y: 60, w: 120, h: 30, text: 'Inc', onclick: Increment }),
// also readable globally: state.count
),
});state ──view(state)──► nodes ──render──► raygui
▲ │
└──── dispatch(action)◄─┘ (onclick / oninput / onchange)See examples/reactive-app.js (npm run example:reactive).
Vertical scroll
Uses raygui GuiScrollPanel plus scissor clipping:
scrollPanel({
x: 24, y: 72, w: 400, h: 360,
contentHeight: 1200, // taller than panel → vertical scrollbar
scrollY: state.scrollY,
onscroll: (s, { scrollY }) => ({ ...s, scrollY }),
children: [
// coords relative to content top-left (0, 0)
label({ x: 12, y: 12, w: 200, h: 20, text: 'Row 1' }),
],
})Run npm run example:scroll.
Images (raylib textures)
const path = require('path');
image({
x: 24, y: 40, w: 200, h: 200,
src: path.join(__dirname, 'assets/logo.png'),
fit: 'contain', // fill | contain | cover | none
// optional sprite / atlas region in original PNG pixels:
rectX: 0, rectY: 0, rectW: 64, rectH: 64,
})Run npm run example:image.
Icons (raygui)
raygui embeds a pack of 16×16 icons. Prefix control labels with #id# or use helpers:
const { Icons, iconText } = require('debloatui/icons');
button({
x: 20, y: 40, w: 120, h: 30,
text: iconText(Icons.FILE_OPEN, 'Open'), // "#5#Open"
onclick: Inc,
});Run npm run example:icons
npm run example:icon-grid # full icon pack in a scroll grid.
Multi-line text
textBlock({
x: 24, y: 48, w: 400, h: 200,
wrap: 'word', // 'none' | 'char' | 'word'
text: 'Hello.\n\nA longer paragraph that wraps inside the box.',
})Run npm run example:multiline
npm run example:table
npm run example:components
npm run example:icons # raygui built-in icons # function components (ActionCard) # Spain squad data table + scroll.
Layout (grid)
Pure JS helper — assigns x/y/w/h to reactive children:
const { gridLayoutContainer, gridSize } = require('debloatui/layout');
const grid = {
x: 24, y: 56, width: 432,
cols: 3, rows: 4,
cellHeight: 48, cellMargin: 10,
};
const nodes = gridLayoutContainer({
grid,
children: [
button({ text: 'A', onclick: Inc }),
button({ text: 'Wide', col: 0, row: 1, colSpan: 2, onclick: Inc }),
],
});
// ...h(...nodes)Run npm run example:layout.
Keyboard events
On the window options object (both run({ ... }) and reactive app({ window })):
const Keys = require('debloatui/keys');
run({
title: 'App',
width: 400,
height: 200,
keydown: (e) => {
if (e.key === Keys.ESCAPE) { /* ... */ }
},
keyup: (e) => { /* ... */ },
}, (ui) => { /* draw */ });Event shape: { type, key, repeat, ctrl, shift, alt, meta }.key is a raylib key code; use debloatui/keys for named constants (Keys.A, Keys.ENTER, …).
Run npm run example:keys.
Mouse events
On the same window options object:
const Mouse = require('debloatui/mouse');
run({
title: 'App',
mousemove: (e) => { /* e.x, e.y, e.dx, e.dy */ },
mousedown: (e) => { /* e.button, e.x, e.y */ },
mouseup: (e) => {},
click: (e) => { /* left button release */ },
rightclick: (e) => { /* right button release */ },
}, (ui) => { /* draw */ });Event shape: { type, button, x, y, dx, dy, ctrl, shift, alt, meta } (dx/dy set on mousemove).
Button codes: debloatui/mouse (Mouse.LEFT, Mouse.RIGHT, …).
Run npm run example:mouse.
Project layout
debloatui/
├── package.json
├── CMakeLists.txt # native addon build
├── native/src/ # N-API + raylib/raygui
│ ├── addon.cpp
│ ├── app.cpp / app.h
│ └── ui_context.cpp / .h
├── lib/
│ ├── index.js / index.d.ts # low-level API
│ ├── reactive.js / .d.ts # state + view + dispatch
│ └── layout.js / .d.ts # grid layout helper
├── examples/
│ ├── basic.js
│ ├── counter.js
│ ├── reactive-app.js
│ ├── reactive-scroll.js
│ ├── reactive-image.js
│ ├── reactive-layout.js
│ └── assets/
├── raygui/ # standalone C sample (no Node)
└── temp/ # experimentsAPI (MVP)
run(options, frameCallback) — blocks until the window closes.
Ui methods (inside frameCallback):
| Method | Returns |
|--------|---------|
| button(x,y,w,h,text) | boolean clicked |
| label(x,y,w,h,text) | void |
| checkBox(x,y,w,h,text,checked) | new boolean |
| slider / sliderBar / progressBar | new number |
| textBox(id,x,y,w,h,text,maxLen?) | { text, active } |
| valueBox / spinner | new number |
| comboBox(x,y,w,h,"A;B",active) | index |
| toggle(...) | new boolean |
| messageBox(..., "Yes;No") | button index or -1 |
| scrollPanel(...) | { scrollX, scrollY, view } |
| image(x,y,w,h,src,options?) | { ok, width, height, rect } |
| beginScissor / endScissor | void |
| statusBar / groupBox / panel / line / dummy | void |
| getScreenSize() | { width, height } |
| getFrameTime() | seconds |
| close() | request quit |
See lib/index.d.ts and lib/reactive.d.ts for full typings.
Design notes
- Debloat: no Chromium, no retained DOM — native raylib/raygui drawing.
- Fast shipping: product logic in JS/TS without rebuilding C++ for every change.
- Performance: one JS entry per frame; control calls stay in-process (N-API).
run()blocks the Node event loop by design (native main loop). Use workers or deferred I/O if you need concurrency.- Desktop-first: Linux, Windows, macOS today; Android planned.
Performance measurement
Sample CPU and memory (RSS) while an example runs (no extra npm deps):
# default: 10s sample window after 1s warmup
npm run measure -- examples/reactive-layout.js
# custom duration / interval
node scripts/measure-example.js examples/reactive-multiline.js -d 15 -i 250
# JSON summary
npm run measure -- examples/reactive-app.js --jsonThe script spawns the example, samples periodically (Linux: /proc), then sends SIGTERM.
Test environment
Results below were captured on this host:
| Spec | Value |
|------|--------|
| OS | Ubuntu 22.04.5 LTS (Jammy) |
| Kernel | Linux 6.8.0-134-generic x86_64 |
| CPU | 13th Gen Intel Core i5-1334U (12 logical CPUs) |
| RAM | ~24 GiB system memory |
| GPU | Intel integrated graphics (Mesa OpenGL) |
| Display | X11 (DISPLAY=:1) |
| Node | v24.18.0 |
| Toolkit | debloatui native addon + raylib 5.5 / raygui (desktop OpenGL) |
This is a developer workstation, not a constrained low-end device. Absolute MiB will differ on smaller machines; the point of the comparison is the single-process, no-Chromium cost class.
Sample results (debloatui)
Settings: warmup 1 s, duration 10 s, interval 200 ms, window open and idle (no heavy interaction).
CPU is approximate % of one core from process tick deltas.
| Example | RSS min (MiB) | RSS avg (MiB) | RSS max (MiB) | CPU avg (%) | CPU max (%) |
|---------|---------------|---------------|---------------|-------------|-------------|
| examples/basic.js | 113.6 | 113.6 | 113.8 | 5.6 | 10 |
| examples/reactive-layout.js | 115.1 | 117.6 | 118.4 | 6.2 | 10 |
| examples/reactive-multiline.js | 113.9 | 115.4 | 116.5 | 6.8 | 15 |
Takeaways from this run:
- Small tools land around ~114–118 MiB RSS for a live OpenGL window + Node + native UI.
- Idle CPU stays low (single-digit average % of one core at 60 FPS target with a simple scene).
- Cost is one process (Node loads
debloatui.node); there is no separate browser renderer/GPU process tree.
Re-run on your machine to refresh the table:
npm run measure -- examples/reactive-layout.js -d 10 -i 200 --jsonComparison with traditional WebView UI
A typical “WebView desktop UI” means Electron, CEF, or an OS WebView (WebView2, WKWebView, WebKitGTK) hosting HTML/CSS/JS. Those stacks attach a full browser engine. They are a great fit for complex web UIs, but idle cost and install size scale differently from an immediate-mode native UI.
| Stack | Typical memory for a simple / idle UI | Notes | |-------|----------------------------------------|--------| | debloatui (measured above) | ~114–118 MiB RSS | Single process: Node + raylib/raygui | | Electron (empty / “hello” window) | Often ~150–300+ MiB across processes | Chromium multi-process model (main + GPU + renderer); community and vendor samples commonly land in this range | | WebView2 / WebKitGTK thin host | Often ~50–150+ MiB, highly variable | Still a browser engine; grows with DOM/CSS/JS complexity | | Empty Chrome/Chromium tab | Often ~50–100+ MiB per tab process | Reference only — not a complete app shell |
How to read this fairly:
- WebView/Electron figures are order-of-magnitude ranges from public docs and community measurements, not a same-app A/B bench inside this repository.
- Absolute numbers change with OS, GPU driver, scaling, and whether the window is focused.
- For forms, tools, and simple control panels, debloatui stays in a compact single-process band without shipping Chromium.
- Prefer a WebView when you need full HTML/CSS/DOM, the web widget ecosystem, or an existing web front-end; prefer debloatui when you want native drawing, low bloat, and JS/TS logic.
Install / runtime size (qualitative): Electron apps often ship ~100–200+ MB of runtime binaries. debloatui ships a small native addon (plus system OpenGL/X11 or OS equivalents), with Node provided by the environment rather than embedded Chromium.
License
debloatui is licensed under the GNU GPL v3 or later — see LICENSE.md.
Dependencies raylib and raygui use zlib-style licenses (GPL-compatible) — see their repositories.
