Overview
PocketJS is a JSX UI stack for the Sony PSP (and beyond). You write components
with Solid and style them with a Tailwind subset; a build step compiles
your styles and bakes your fonts, and the whole thing runs on top of a single
no_std Rust core that does flexbox layout, styling, animation, text, and
rendering. The same app.tsx runs on real PSP hardware, in the PPSSPP
emulator, in the browser, and headless under Bun.
If you know React or Solid, you already know most of PocketJS. The primitives
are View, Text, and Image; state is createSignal / createEffect;
layout and color come from class strings like flex-col items-center gap-4 bg-slate-50. What is different is what happens underneath: there is no DOM, no
VDOM, and no runtime CSS.
One core, four hosts
Everything renders through one Rust core, pocketjs-core — a
platform-agnostic #![no_std] library that owns the retained node tree,
taffy flexbox layout, the style table,
animation tracks, baked text, and a DrawList. That core is compiled twice and
paired with a backend per host:
| Host | JS engine | Rust build | Backend |
|---|---|---|---|
| PSP hardware | QuickJS | mipsel-sony-psp |
sceGu (the GE) |
| PPSSPP | QuickJS | mipsel-sony-psp |
sceGu, run headless for e2e goldens |
| Browser | the browser's | wasm32-unknown-unknown |
deterministic software rasterizer → canvas |
| Headless Bun | Bun | wasm32-unknown-unknown |
same rasterizer → byte-exact PNG goldens |
Layout runs in exactly one place — the Rust core — so a screen lays out the
same everywhere. The browser and Bun hosts share one rasterizer, which is what
makes deterministic golden images possible. See
Architecture for the full picture and
Native contract for the ui.* op set that bridges JS
and Rust.
Who it's for
PocketJS is for JavaScript and TypeScript developers who want to build real, animated UI for the PSP — launchers, menus, dashboards, small apps — without writing C or hand-rolling a layout engine. You get a familiar reactive component model and utility-class styling; the framework handles host detection, the generated style table, image uploads, and the per-frame host callback for you.
Three pillars
1. Solid, with no VDOM. PocketJS drives Solid through its universal
renderer (babel-preset-solid with generate: 'universal'). Solid has no
virtual DOM: when a signal changes, only the effect closures that read it
re-run, and those closures issue mutation ops straight to the core tree. There
is no diffing pass. See Reactivity.
2. A build-time Tailwind-subset compiler, with zero runtime CSS. Class
strings are parsed at build time. A literal like class="p-2 rounded-md bg-blue-600" compiles to a numeric style record iff every whitespace-
separated token is a supported utility; the compiler writes a binary style
table (styles.bin) plus a generated lookup, and at runtime a class is just a
styleId. There is no CSS engine on the device. Dynamic styling is expressed as
ternaries of whole class literals, style={{…}} objects, or animate().
classList, hover:, and template-interpolated class fragments are compile
errors, not silent no-ops. See Styling and
Tailwind subset.
3. Baked font atlases. Text uses Inter (OFL), baked into atlases at build time. The build scans your source for the exact characters and font sizes you actually use and rasterizes only those atlas slots — supersampled 8-bit coverage cells with proportional advances and a cmap. There is no font rasterizer on the device; drawing text is compositing pre-baked coverage. See Build pipeline.
Hello, counter
A complete app: a counter you bump by pressing a focusable button.
// app.tsx
import { Text, View } from "@pocketjs/core/components";
import { createSignal } from "@pocketjs/core/reactivity";
export default function App() {
const [count, setCount] = createSignal(0);
return (
<View class="flex-col items-center gap-4 p-4 bg-slate-50">
<Text class="text-xl text-slate-950">Count: {count()}</Text>
<View
class="p-2 rounded-md bg-blue-600 focus:bg-blue-500 transition-colors duration-150"
focusable
onPress={() => setCount(count() + 1)}
/>
</View>
);
}The mount entry is ordinary bootstrap code — the framework detects the host, loads the generated style table and dcpak assets, and wires up the frame callback:
// main.tsx
import { mount } from "@pocketjs/core";
import App from "./app.tsx";
mount(() => <App />);Build it with Bun:
bun scripts/build.ts hero # -> dist/hero.js + dist/hero.dcpakA few things worth noticing in that example:
focus:bg-blue-500is a variant baked into the style record. Focus changes swap styles natively, with zero JS on the focus transition.transition-colors duration-150declares motion; the tween ticks in Rust at a fixeddt = 1/60 s. JS only declares it.Count: {count()}is a mixed text run — a static string and a reactive expression laid out as one inline run, not two flex items.
The same source, everywhere
That one app.tsx is what runs on every host:
- PSP hardware — bundled into an EBOOT, QuickJS evaluating your JS, the core driving sceGu.
- PPSSPP — the same EBOOT, run headless for end-to-end frame goldens stamped with the emulator build.
- Browser — the core compiled to WASM with a software rasterizer, drawn to a 480×272 canvas. Try demos in the Playground.
- Headless Bun — the same WASM rasterizer, scripted input, fixed timestep, producing byte-exact PNG goldens for the test suite.
Because animation ticks at a fixed dt and frame content is a pure function of
the frame index, goldens are byte-exact rather than approximate.
What v1 punts
PocketJS v1 is deliberately scoped. It does not yet include:
- Kinetic / momentum scroll views
hover:variants (there is no pointer on a PSP)- Percentage sizes beyond
-full rounded-fullon runtime-sized nodes — it requires build-time-knownw-N h-Nin the same class literal- CLUT / swizzled textures
- Render-to-texture opacity groups (per-vertex alpha is used instead — wrong on overlap, fine for demos)
- Kerning
- 3DS / Android hosts
These are omissions, not silent failures: unsupported class tokens and disallowed patterns surface as loud compile-time or dev errors. See the full list in the Tailwind subset reference.
Next steps
- Getting started — install, build, and run your first app.
- Architecture — how the JS runtime, the Rust core, and the four backends fit together.
- Components —
View,Text,Image, control flow, and the app-shell primitives. - Playground — run the demos in your browser.