PocketJS styling is a build-time Tailwind subset. The classes you write are not CSS: at build time the compiler parses each class literal, turns it into a binary style record, and packs the whole table intoDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/pocket-stack/pocketjs/llms.txt
Use this file to discover all available pages before exploring further.
styles.bin inside the app’s .pak. At runtime the renderer looks a class attribute up by its exact string, gets back a numeric styleId, and hands that to the Rust core via setStyle. There is zero runtime CSS — no CSS parser, no cascade, no string matching on the device. A class is just an integer index into a table that was resolved and frozen at build time.
The Pipeline
The Tailwind compiler runs in pass 1 ofbun scripts/build.ts <app>:
- The Babel pass collects candidate class strings from the AST — every string literal, every template-literal quasi, and every chunk of JSX text.
compiler/tailwind.tstries to parse each candidate. The ones that parse become style records; identical records are deduplicated to a singlestyleId.- The records are encoded to
styles.binand a generatedstyles.generated.tsmodule (STYLE_IDS: the class-literal →styleIdmap, plus font-slot metadata) that the renderer imports.
styles.bin is shipped in the .pak. On PSP the native pak walker feeds it directly into the Rust core; on the browser and headless Bun hosts it is loaded through the loadStyles op.
The All-or-Nothing Rule
The compiler sees every string literal in your source, not justclass attributes — so it needs a strict rule for deciding which strings are actually styles:
A class literal compiles to a style record if and only if every whitespace-separated token is a supported utility. If any token is not a utility, the whole literal is treated as ordinary text and silently ignored.
"flex the muscles" never accidentally becomes a layout rule. The flip side is that a class attribute must be a literal string the compiler can see — the renderer resolves it by exact string match against STYLE_IDS.
Dynamic Styling
Styles are frozen at build time, so you cannot build a class string at runtime. There are exactly three supported approaches:1. Ternaries of Full Class Literals
Both branches of the ternary must be complete string literals that the compiler can see and parse independently:styleId when the signal changes, which also triggers any transitions declared in the new record.
2. style={{ ... }} Objects
An inline style object sets individual style properties at runtime, bypassing the class table. Each key is diffed against the previous value and pushed as a single setProp call. Use this for values you cannot know at build time — pixel-perfect widths, animation start positions, reactive offsets:
translateX, translateY, scale, rotate) where possible — they animate without triggering relayout.
3. animate() and spring()
Declarative motion driven natively per vblank. See the Animation guide for details.
focus: and active: Variants
focus: and active: are folded into the same style record as the base styles. When focus or the pressed state changes, the Rust core switches to the matching variant natively — zero JS runs on the state change.
focus:bg-blue-500 and active:bg-blue-700 live in the same compiled record as the base styles. The native core applies the focus: block when setFocus is called and the active: block while the press is held — no JavaScript, no reconciliation. Any transition-* utilities in the record run natively on the variant swap.
From the settings demo, a toggle row that changes background and border on focus:
Transitions
Addingtransition-* utilities to a class attaches a transition block to that style record. Transitions fire whenever the style record is swapped — on focus:/active: variant changes and when a dynamic class ternary swaps one full literal for another. The Rust core tweens only the animatable props that actually changed between the old and new style.
transition, transition-colors, transition-transform, transition-opacity, transition-all, duration-N, ease-*, and delay-N are all supported at the class level. See the Animation guide for the full set and the animate() imperative API.
How style={{ }} Works Internally
Each key in the style object is a PropName from the spec. The renderer calls setProp(id, propId, value) for every key that changed since the last render, one setProp per changed key. The propId is a small integer from the spec’s PROP table. On PSP this is a native FFI dispatch that costs essentially nothing per call.
rounded-full Needs a Known Size
rounded-full bakes an exact pixel radius at build time — the compiler must be able to compute min(w, h) / 2. That means the same literal must contain both w-N and h-N (or their arbitrary-px forms):
rounded-full appears without a concrete w-N/h-N in the same literal, it is a loud compile error, not a silent drop. rounded-full on runtime-sized nodes is out of scope for v1.
Token Order Does Not Matter
Within a literal, tokens are deduplicated (last-wins per property) and sorted into a canonical order by property id. Two literals with the same tokens in different order share onestyleId:
Loud Errors
Three patterns look like styling but are not supported. They fail the build with a code frame rather than silently doing nothing:| Pattern | Why it fails | Do this instead |
|---|---|---|
classList={{ ... }} | Not supported in v1. | Ternary of full literals. |
class={`a ${b}`} (template-interpolated) | Styles resolve at build time; a fragment isn’t a statically knowable literal. | Ternary of full literals. |
hover:… | The PSP has no pointer. | Use focus: / active:. |
classList and template-interpolation errors are raised by the Babel pass. hover: is raised by the Tailwind compiler when it confirms every other token in the literal is valid. A stray word like "hover over here" is just treated as plain text (all-or-nothing rule), so it only errors when it genuinely parses as a hover: variant on an otherwise valid class literal.
See Also
- Tailwind Subset Reference — the complete supported-utility list
- Animation —
transition-*,animate(), andspring() - Input & Focus —
focus:/active:variant semantics