Skip to main content

Documentation 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.

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 into 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 of bun scripts/build.ts <app>:
  1. The Babel pass collects candidate class strings from the AST — every string literal, every template-literal quasi, and every chunk of JSX text.
  2. compiler/tailwind.ts tries to parse each candidate. The ones that parse become style records; identical records are deduplicated to a single styleId.
  3. The records are encoded to styles.bin and a generated styles.generated.ts module (STYLE_IDS: the class-literal → styleId map, 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 just class 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-col items-center gap-4 p-4"  // every token is a utility → style record
"Ready to play"                    // no token is a utility   → plain text, ignored
"flex the muscles"                 // one bad token ("the")   → ignored entirely
This is why a label like "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.
When any single token in a class literal is unrecognized, the entire literal is silently dropped. If your styling is not applying, check that every token is on the supported utilities list.

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:
import { View } from "@pocketjs/framework/components";
import { createSignal } from "solid-js";

const [armed, setArmed] = createSignal(false);

<View class={armed() ? "p-2 bg-red-500" : "p-2 bg-slate-700"} />;
Each branch is compiled independently. The renderer swaps the resolved 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:
import { View } from "@pocketjs/framework/components";

// Width driven by a signal — unknown at build time
<View
  class="h-2 rounded-full bg-gradient-to-r from-emerald-500 to-emerald-600"
  style={{ width: (position() / TRACK_FRAMES) * 160 }}
/>;
Prefer transform keys (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.
import { View, Text } from "@pocketjs/framework/components";

<View
  class="p-2 rounded-md bg-blue-600 focus:bg-blue-500 active:bg-blue-700 transition-colors duration-150"
  focusable
  onPress={() => {}}
>
  <Text class="text-base text-white font-bold">Press Circle</Text>
</View>
Here 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:
<View
  class="flex-row items-center justify-between px-2 py-1 bg-white border-indigo-200 rounded-lg shadow focus:bg-indigo-50 focus:border-indigo-500 transition-colors duration-150"
  focusable
  onPress={onToggle}
>
  {/* … */}
</View>

Transitions

Adding transition-* 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.
// Background fades on focus — no JS per frame, Rust owns the tween
<View class="bg-slate-700 focus:bg-slate-500 transition-colors duration-200 ease-out" />
The motion utilities 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):
// ✅ OK — radius baked to 24px
<View class="w-12 h-12 rounded-full bg-slate-700" />

// ❌ Compile error — no build-time width and height
<View class="rounded-full bg-slate-700" />
If 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 one styleId:
"p-2 bg-red-500"  // same style record
"bg-red-500 p-2"  // same styleId — deduped across the whole app

Loud Errors

Three patterns look like styling but are not supported. They fail the build with a code frame rather than silently doing nothing:
PatternWhy it failsDo 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:.
// All three throw at build time:
<View classList={{ "bg-red-500": armed() }} />;
<View class={`p-2 ${bg}`} />;
<View class="p-2 hover:bg-blue-500" />;
The 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

Build docs developers (and LLMs) love