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 renders every UI from a small set of typed components imported from @pocketjs/framework/components. There are exactly three host primitives — View, Text, and Image — plus a Sprite primitive for animated atlases. All higher-level app-shell components (Screen, Modal, Gallery, and friends) are built on top of these and live in the same package. Control flow helpers (Show, For, Index, Switch, Match) are imported from solid-js in Solid apps, or expressed directly in Vue Vapor’s JSX.
<view> (lowercase) is an internal renderer target and is not a JSX intrinsic. It is deliberately excluded from the type declarations so that a stray <view> in application code fails the TypeScript check. Always use the capitalized View.
View
View is the only container primitive. It lays out its children using flexbox (via Taffy), carries compiled Tailwind-subset styling via class, and can register with the focus manager via focusable.
import { View, Text, Image } from "@pocketjs/framework/components";
<View class="flex-row items-center gap-3 p-5 bg-slate-50">
<Image class="w-10 h-10 rounded-lg" src="logo.png" />
<View class="flex-col">
<Text class="text-base text-slate-950 font-bold">PocketJS</Text>
<Text class="text-xs text-slate-500">SOLID + RUST + SCEGU</Text>
</View>
</View>
import { View, Text, Image } from "@pocketjs/framework/components";
<View class="flex-row items-center gap-3 p-5 bg-slate-50">
<Image class="w-10 h-10 rounded-lg" src="logo.png" />
<View class="flex-col">
<Text class="text-base text-slate-950 font-bold">PocketJS</Text>
<Text class="text-xs text-slate-500">VUE VAPOR + RUST + SCEGU</Text>
</View>
</View>
A View becomes interactive by adding focusable and an onPress handler. onPress fires when the node is focused and the user presses the Circle button (the PSP’s confirm button). Always pair onPress with focusable — an unfocusable node never receives input directly, though it can still act as a bubble target if a focused descendant has no handler.
import { View, Text } from "@pocketjs/framework/components";
import { createSignal } from "solid-js";
function PressButton() {
const [count, setCount] = createSignal(0);
return (
<View
class="px-4 py-2 rounded-xl bg-blue-600 focus:bg-blue-500 active:bg-blue-700 transition-colors duration-150"
focusable
onPress={() => setCount(count() + 1)}
>
<Text class="text-base text-white font-bold">Press Circle</Text>
</View>
);
}
import { View, Text } from "@pocketjs/framework/components";
import { ref } from "vue";
function PressButton() {
const count = ref(0);
return () => (
<View
class="px-4 py-2 rounded-xl bg-blue-600 focus:bg-blue-500 active:bg-blue-700 transition-colors duration-150"
focusable
onPress={() => { count.value++; }}
>
<Text class="text-base text-white font-bold">Press Circle</Text>
</View>
);
}
Props
ViewProps, TextProps, and ImageProps are exported from @pocketjs/framework/components for typing your own wrapper components.
| Prop | View | Text | Image | Type | Notes |
|---|
class | ✓ | ✓ | ✓ | string | Compiled Tailwind-subset class literal. |
style | ✓ | ✓ | ✓ | Record<string, number | string> | Per-key dynamic style object. |
children | ✓ | ✓ | | JSX.Element | Image takes no children. |
focusable | ✓ | | | boolean | Registers the node with the focus manager. |
onPress | ✓ | | | () => void | Fires when focused and Circle is pressed. |
ref | ✓ | ✓ | ✓ | (node: NodeMirror) => void | NodeMirror | Handle to the underlying mirror node. |
nodeRef | ✓ | ✓ | ✓ | (node: NodeMirror) => void | Callback ref for Vue Vapor (avoids JSX ref collision). |
src | | | ✓ | string | Baked texture name from the pak. |
The style prop
class is compiled ahead of time. Use style for values only known at runtime — it sets individual style keys directly, diffed per key on each render:
const TRACK_FRAMES = 600;
<View
class="h-2 rounded-full bg-gradient-to-r from-emerald-500 to-emerald-600"
style={{ width: (position() / TRACK_FRAMES) * 160 }}
/>
const TRACK_FRAMES = 600;
<View
class="h-2 rounded-full bg-gradient-to-r from-emerald-500 to-emerald-600"
style={{ width: (position.value / TRACK_FRAMES) * 160 }}
/>
Prefer transform keys (translateX, translateY, scale, rotate) for motion — they animate without triggering relayout.
The ref prop
ref gives you the underlying NodeMirror, which you pass to imperative APIs like animate(). Both Solid ref forms work — a plain variable (Solid assigns it) or a callback:
import { View, type NodeMirror } from "@pocketjs/framework/components";
import { animate } from "@pocketjs/framework/animation";
import { onMount } from "solid-js";
function Underline() {
let underline: NodeMirror | undefined;
onMount(() => {
if (underline) animate(underline, "width", 210, { dur: 700, easing: "out" });
});
return <View ref={underline} class="h-1 w-0 rounded-full bg-blue-500" />;
}
import { View, type NodeMirror } from "@pocketjs/framework/components";
import { animate } from "@pocketjs/framework/animation";
import { onMounted } from "vue";
function Underline() {
let underline: NodeMirror | undefined;
onMounted(() => {
if (underline) animate(underline, "width", 210, { dur: 700, easing: "out" });
});
return () => (
<View
nodeRef={(node) => { underline = node ?? undefined; }}
class="h-1 w-0 rounded-full bg-blue-500"
/>
);
}
Text
Text renders type. Its children are laid out as a single inline run — one measured line, not separate flex items — so you can freely mix static strings and reactive expressions:
import { Text } from "@pocketjs/framework/components";
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
return <Text class="text-sm text-slate-600">Count: {count()}</Text>;
}
import { Text } from "@pocketjs/framework/components";
import { ref } from "vue";
function Counter() {
const count = ref(0);
return () => <Text class="text-sm text-slate-600">Count: {count.value}</Text>;
}
Count: and the reactive value are concatenated and measured together. When the signal or ref changes, only the text content updates via replaceText — no relayout unless the measured width actually changes.
Text style — font slot, color, tracking, alignment — comes from the class on the <Text> element itself. Available text sizes map to baked font-atlas slots:
| Utility | Baked px |
|---|
text-xs | 12 |
text-sm | 14 |
text-base | 16 |
text-lg | 18 |
text-xl | 20 |
text-2xl | 24 |
text-4xl | 36 |
Empty text and layout
An empty text node — such as the placeholder Solid emits for a <Show> that is currently false — is excluded from layout entirely. It contributes no width, height, or gap. It re-enters layout the moment it becomes non-empty. This is why toggling a <Show> inside a gap-N row leaves no phantom gap where the hidden element was.
Image
Image draws a baked texture. Its src is an asset name, not a path or URL. At build time the pipeline scans src strings, packs referenced images into the app’s .pak, and the renderer resolves the name to an uploaded texture at runtime.
import { Image } from "@pocketjs/framework/components";
<Image class="w-10 h-10 rounded-lg shadow" src="logo.png" />
Set the drawn size with box utilities (w-10 h-10); class controls layout, src controls pixels. src is reactive — assigning a new name swaps the texture via setImage.
Sprite
Sprite is an auto-playing animated sprite atlas. Backed by an image node whose sprite atlas key the Rust core cycles every vblank — deterministic, zero per-frame JS. It plays from the first frame the moment it is displayed, so revealing or paging a Sprite starts its animation automatically.
import { Sprite } from "@pocketjs/framework/components";
<Sprite class="w-12 h-12" sprite="explosion" />
For signal-driven frame control (e.g. a loading spinner), use Image with createSpriteAnimation:
import { Image } from "@pocketjs/framework/components";
import { createSpriteAnimation } from "@pocketjs/framework/lifecycle";
const frame = createSpriteAnimation(
["spinner-00.svg", "spinner-01.svg", "spinner-02.svg"],
{ frameStep: 3 },
);
<Image class="w-10 h-10" src={frame()} />;
import { Image } from "@pocketjs/framework/vue-vapor/components";
import { createSpriteAnimation } from "@pocketjs/framework/vue-vapor/lifecycle";
const frame = createSpriteAnimation(
["spinner-00.svg", "spinner-01.svg", "spinner-02.svg"],
{ frameStep: 3 },
);
<Image class="w-10 h-10" src={frame.value} />;
Control Flow
In Solid apps, render lists and conditionals with Solid’s control-flow components rather than array.map + &&. Import them directly from solid-js. Vue Vapor apps use Vue’s native JSX control-flow patterns instead.
Show
Toggles a subtree on a boolean condition, with an optional fallback:
import { Show } from "solid-js";
import { Text } from "@pocketjs/framework/components";
<Show
when={count() > 3}
fallback={<Text class="text-sm text-slate-500">Keep going…</Text>}
>
<Text class="text-sm text-emerald-600">Reactive on real hardware.</Text>
</Show>
import { Text } from "@pocketjs/framework/components";
{count.value > 3 ? (
<Text class="text-sm text-emerald-600">Reactive on real hardware.</Text>
) : (
<Text class="text-sm text-slate-500">Keep going...</Text>
)}
For
For renders a list keyed by reference. Its callback receives the item and an index accessor. When the array is reordered, For moves existing nodes to their new positions instead of destroying and recreating them. Focus, animation state, and imperative refs survive a move.
import { For } from "solid-js";
import { View, Text } from "@pocketjs/framework/components";
<For each={items()}>
{(item, i) => (
<View
class="flex-row items-center gap-3 p-1 rounded-lg shadow bg-white border-slate-200 focus:bg-blue-50 focus:border-blue-500 transition-colors duration-150"
focusable
onPress={() => select(i())}
>
<Text class="text-xs text-slate-950 font-bold">{item.title}</Text>
<Text class="text-xs text-slate-500">{item.time}</Text>
</View>
)}
</For>
import { View, Text } from "@pocketjs/framework/components";
{items.value.map((item, i) => (
<View
class="flex-row items-center gap-3 p-1 rounded-lg shadow bg-white border-slate-200 focus:bg-blue-50 focus:border-blue-500 transition-colors duration-150"
focusable
onPress={() => select(i)}
>
<Text class="text-xs text-slate-950 font-bold">{item.title}</Text>
<Text class="text-xs text-slate-500">{item.time}</Text>
</View>
))}
Index
Index is keyed by position. The item is an accessor, the index is a plain number. Use it when the list length is stable and only the values at each slot change (equalizer bars, a fixed set of rows).
import { Index } from "solid-js";
import { View } from "@pocketjs/framework/components";
<Index each={bars()}>
{(bar, i) => (
<View class="w-2 rounded-md bg-emerald-500" style={{ height: bar() }} />
)}
</Index>
import { View } from "@pocketjs/framework/components";
{bars.value.map((bar) => (
<View class="w-2 rounded-md bg-emerald-500" style={{ height: bar }} />
))}
Switch / Match
Pick one of several branches:
import { Switch, Match } from "solid-js";
import { Text } from "@pocketjs/framework/components";
<Switch fallback={<Text>Idle</Text>}>
<Match when={state() === "loading"}><Text>Loading…</Text></Match>
<Match when={state() === "ready"}><Text>Ready.</Text></Match>
</Switch>
import { Text } from "@pocketjs/framework/components";
{state.value === "loading" ? (
<Text>Loading...</Text>
) : state.value === "ready" ? (
<Text>Ready.</Text>
) : (
<Text>Idle</Text>
)}
App-Shell Primitives
@pocketjs/framework/components also exports a layer of higher-level components that compose View with focus, overlay, and layout behavior.
Screen
A full-screen flex container with sensible defaults. The default class is "relative flex-col w-full h-full bg-slate-50 overflow-hidden". Pass a custom class to override.
import { Screen, Text } from "@pocketjs/framework/components";
<Screen>
<Text class="text-base text-slate-950 font-bold">Hello PocketJS</Text>
</Screen>
Focusable
A shorthand for View with focusable={true} preset. All ViewProps pass through.
import { Focusable, Text } from "@pocketjs/framework/components";
<Focusable
class="px-4 py-2 rounded-lg bg-slate-200 focus:bg-sky-500"
onPress={() => startGame()}
>
<Text class="text-slate-900">Play</Text>
</Focusable>
FocusScope
FocusScope captures d-pad traversal to a subtree for the lifetime of the component. When active is truthy it calls pushFocusScope on the root node under the hood, auto-focusing the first focusable child and restoring the previous focus on dispose.
import { FocusScope, Text } from "@pocketjs/framework/components";
<FocusScope class="flex-col gap-2 p-3 rounded-xl bg-white">
<Text class="text-sm text-slate-600">Navigation is trapped here</Text>
</FocusScope>
Options (active, autoFocus, restoreFocus) mirror pushFocusScope. See Input & Focus for details on focus scope semantics.
FocusGrid
FocusGrid overlays explicit row/column d-pad traversal on a subtree. LEFT/RIGHT move within a row; UP/DOWN move between rows. The columns prop sets how many items per row; wrap enables edge-wrapping.
import { FocusGrid, View } from "@pocketjs/framework/components";
<FocusGrid columns={4} wrap class="flex-row flex-wrap gap-2">
{tiles.map((tile) => (
<View class="w-16 h-16 rounded-lg bg-slate-200 focus:bg-blue-500" focusable />
))}
</FocusGrid>
ActionHandler
ActionHandler binds a button to a handler without rendering a visible node. It wraps onButtonPress and cleans up on unmount. Use it for global shortcuts or secondary buttons on a screen.
import { ActionHandler } from "@pocketjs/framework/components";
import { BTN } from "@pocketjs/framework/input";
<ActionHandler button={BTN.CROSS} onPress={() => goBack()} />
The active prop gates the handler on/off (accepts a boolean or reactive accessor). allowWhenBlocked keeps it firing even when a modal blocks background input.
Portal
Portal mounts its children into the overlay root — a full-screen, z-index: 1000, absolutely-positioned node that sits above all screen content. Used internally by Modal and ActionBar.
import { Portal, View, Text } from "@pocketjs/framework/components";
<Portal>
<View class="absolute bottom-3 left-3 right-3 px-3 py-2 rounded-lg bg-slate-800 shadow-lg">
<Text class="text-xs text-white">This is in the overlay layer</Text>
</View>
</Portal>
Modal
Modal combines a Portal, a semi-transparent backdrop, and a FocusScope. While open is truthy the backdrop fades to 62% opacity, the panel appears, and pushButtonHandlerBlock silences all background button handlers so only the modal content receives input.
import { Modal, View, Text, Focusable } from "@pocketjs/framework/components";
import { createSignal } from "solid-js";
function ConfirmDialog() {
const [open, setOpen] = createSignal(false);
return (
<>
<Focusable
class="px-4 py-2 rounded-lg bg-blue-600 focus:bg-blue-500"
onPress={() => setOpen(true)}
>
<Text class="text-base text-white font-bold">Open Modal</Text>
</Focusable>
<Modal open={open()}>
<Text class="text-sm text-slate-950 font-bold">Confirm?</Text>
<View class="flex-row gap-2">
<Focusable
class="px-3 py-1 rounded-lg bg-blue-600 focus:bg-blue-500"
onPress={() => { setOpen(false); }}
>
<Text class="text-sm text-white">Yes</Text>
</Focusable>
<Focusable
class="px-3 py-1 rounded-lg bg-slate-200 focus:bg-slate-300"
onPress={() => setOpen(false)}
>
<Text class="text-sm text-slate-900">No</Text>
</Focusable>
</View>
</Modal>
</>
);
}
ActionBar
ActionBar is a portal’d bottom action strip. Its default class docks it to the bottom of the screen as a floating pill:
import { ActionBar, Text } from "@pocketjs/framework/components";
<ActionBar>
<Text class="text-xs text-slate-500">CIRCLE Confirm · CROSS Back</Text>
</ActionBar>
Grid
Grid lays tiles out as a wrapping row (flex-row flex-wrap) and, when columns is set, adds FocusGrid d-pad traversal. Pass gap as a number to set it via the style object while keeping class a single compiled literal.
import { Grid, View } from "@pocketjs/framework/components";
<Grid columns={4} gap={8} wrap>
{items.map((item) => (
<View class="w-16 h-16 rounded-lg bg-slate-200 focus:bg-blue-500" focusable />
))}
</Grid>
Lazy
Lazy gates a subtree on demand. While when is false nothing is built and the native subtree is destroyed. When it becomes true the content mounts, optionally after a reveal frame delay that shows a fallback (spinner or skeleton). The reveal is a one-shot latch — a later re-activation shows content immediately.
import { Lazy, View, Text } from "@pocketjs/framework/components";
<Lazy
when={isVisible()}
reveal={6}
fallback={<Text class="text-xs text-slate-500">Loading…</Text>}
>
{() => (
<View class="flex-col gap-2 p-3 rounded-xl bg-white shadow">
<Text class="text-base text-slate-950 font-bold">Loaded Content</Text>
</View>
)}
</Lazy>
Gallery
Gallery is a full-screen horizontally paged strip. Pressing LTRIGGER pages left; RTRIGGER pages right. Each page slides in with a native translateX tween on the inner strip node — paint-only, no relayout, one FFI call per page turn. Off-window pages are not built, keeping the PSP draw budget in check.
import { Gallery, View, Text } from "@pocketjs/framework/components";
import { createSignal } from "solid-js";
function PagedContent() {
const [page, setPage] = createSignal(0);
return (
<Gallery
count={4}
page={page}
onPageChange={setPage}
duration={300}
easing="out"
renderPage={(i) => (
<View class="w-full h-full flex-col items-center justify-center bg-slate-50">
<Text class="text-4xl text-slate-950 font-bold">Page {i + 1}</Text>
</View>
)}
/>
);
}
Gallery binds LTRIGGER/RTRIGGER internally by default (bindTriggers={true}). Set bindTriggers={false} and manage onPageChange yourself if you need custom shoulder-button behavior.
Summary Table
| Component | Purpose |
|---|
View | Flexbox container. All layout, styling, and input. |
Text | Inline text run. Mixed static + reactive content. |
Image | Baked texture from pak. Size from class, pixels from src. |
Sprite | Auto-playing sprite atlas. Zero per-frame JS. |
Screen | Full-screen root container with sensible defaults. |
Focusable | A View that is focusable by default. |
FocusScope | Traps and restores focus within a subtree. |
FocusGrid | 2-D grid focus navigation (columns, wrap). |
ActionHandler | Binds a button to a handler without rendering a node. |
Portal | Renders children into the overlay root. |
Modal | Backdrop + focus-trapped panel in the overlay. |
ActionBar | Docked bottom bar in the overlay layer. |
Grid | Flex-wrap tile layout with optional FocusGrid. |
Lazy | On-demand mount with optional reveal delay. |
Gallery | Full-screen horizontally paged strip (LTRIGGER/RTRIGGER). |