PocketJS

Components

Everything you render in a PocketJS app is built from a tiny set of components, all imported from a single entry point:

import {
  View, Text, Image,
  Show, For, Index, Switch, Match,
} from "@pocketjs/core/components";

There are exactly three host primitivesView, Text, and Image — plus Solid's control-flow helpers (Show, For, Index, Switch, Match) re-exported for convenience. Higher-level app-shell primitives (Screen, Focusable, Modal, and friends) build on View and are covered on the App shell page.

If you know React Native, the mental model is familiar: View is your box, Text is your typography, Image is your picture. The capitalized names are the public API. Under the hood the renderer targets lowercase view / text / image host tags, but those are an internal detail — they are deliberately not declared as JSX intrinsics, so <view> in app code fails typecheck. Always use the capitalized components.

View

View is the only container primitive. It lays out its children with flexbox (via taffy), carries styling, and can take focus and input.

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

A View becomes interactive by adding focusable and an onPress handler. onPress fires when the node is focused and the user presses the confirm button (Circle):

<View
  class="px-4 py-2 rounded-xl bg-blue-600 focus:bg-blue-500 active:bg-blue-700"
  focusable
  onPress={() => setCount(count() + 1)}
>
  <Text class="text-base text-white font-bold">Press Circle</Text>
</View>

Pair onPress with focusable — an unfocusable node never receives input. See Input & focus for how focus moves between nodes.

Text

Text renders type. A <Text> lays out its string children as one inline run — a single measured line, not N separate flex items — so you can freely mix static text and reactive expressions:

<Text class="text-sm text-slate-600">Count: {count()}</Text>

Count: and {count()} are concatenated and measured together. When the signal changes, only the text content is updated (via the native replaceText op); no relayout happens unless the measured width actually changes.

Text style inheritance

Text nodes inherit their resolved text style — font slot, color, tracking, alignment — from the nearest ancestor that sets text props. In practice this means you put text utilities (text-*, font-bold, tracking-wide, …) on the <Text> element itself:

<Text class="text-4xl text-slate-950 font-bold">JSX at 60 FPS.</Text>

The available text sizes, weights, colors and alignment utilities are baked at build time — see Styling and Tailwind subset for the exact set. Sizes map to baked font-atlas slots (12 / 14 / 16 / 18 / 20 / 24 / 36 px), so only the sizes you actually use are packed into the app.

Empty text and layout

An empty text node — for example the placeholder Solid emits for a <Show> that is currently false — is excluded from layout entirely. It contributes no width, no height, and no gap. It re-enters layout the moment it becomes non-empty. This is why toggling a <Show> inside a gap-N row does not leave a phantom gap where the hidden element used to be.

Image

Image draws a baked texture. Its src is a name, not a path or URL: at build time the pipeline scans your src strings, packs the referenced images into the app's .dcpak, and the renderer resolves the name to the uploaded texture at runtime.

<Image class="w-10 h-10 rounded-lg shadow" src="logo.png" />

Set the drawn size with box utilities (w-10 h-10 above); the class controls layout, src controls pixels. src is reactive — assigning a new name swaps the texture in place (via setImage), which is exactly how sprite animation works:

import { useSpriteAnimation } from "@pocketjs/core/hooks";

const frame = useSpriteAnimation(
  ["spinner-00.svg", "spinner-01.svg", "spinner-02.svg"],
  { frameStep: 3 },
);

<Image class="w-10 h-10" src={frame()} />;

Image takes no children. See the Build pipeline for how images become dcpak textures.

Props

Each primitive has a small, explicit prop interface. ViewProps, TextProps, and ImageProps are exported from @pocketjs/core/components for typing your own wrapper components.

Prop View Text Image Type Notes
class string Compiled Tailwind-subset class string.
style Record<string, number | string> Dynamic per-key style object (see below).
children JSX.Element Image has none.
focusable boolean Registers the node with the focus manager.
onPress () => void Fires when focused and confirmed.
ref (node: NodeMirror) => void | NodeMirror Handle to the mirror node.
src string Baked texture name.

style vs class

class is compiled ahead of time into a fixed style record. style is the escape hatch for values you only know at runtime — it sets individual style keys directly, prev-diffed per key. Use it for signal-driven values:

<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) for motion where you can — they animate without triggering relayout. Full details are on the Styling page.

ref

ref hands you the underlying NodeMirror, which you can pass to imperative APIs like animate(). Both Solid ref forms work — a plain variable (Solid assigns it) or a callback:

import { animate } from "@pocketjs/core/animation";
import { onMount } from "@pocketjs/core/reactivity";
import type { NodeMirror } from "@pocketjs/core/components";

let underline: NodeMirror | undefined;
onMount(() => {
  if (underline) animate(underline, "width", 210, { dur: 700, easing: "out" });
});

<View ref={underline} class="h-1 w-0 rounded-full bg-blue-500" />;

Control flow

Because PocketJS is Solid under the hood, you render lists and conditionals with Solid's control-flow components rather than array.map + &&. They are re-exported verbatim from solid-js, so their semantics are exactly Solid's — but they compile down to native tree-mutation ops on the PSP.

Show

Toggles a subtree on a boolean condition, with an optional fallback:

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

When when flips, the children are inserted or removed from the native tree. While hidden, Show leaves behind only an empty text marker, which — as noted above — takes up no layout space.

For

For renders a list keyed by reference. Its callback receives the item and an index accessor:

<For each={tracks()}>
  {(track, i) => (
    <View class="flex-row justify-between p-1" focusable onPress={() => select(i())}>
      <Text class="text-xs text-slate-900">{track.title}</Text>
      <Text class="text-xs text-slate-500">{track.artist}</Text>
    </View>
  )}
</For>

When the array is reordered, For moves existing nodes to their new positions instead of destroying and recreating them (the native insertBefore op unlinks a node from its old spot before re-inserting it). Focus, animation state, and any imperative refs survive the move. Reach for For whenever list items have stable identity.

Index

Index is the counterpart keyed by position. Here the item is an accessor and the index is a plain number:

<Index each={bars()}>
  {(bar, i) => <View class="w-2 rounded-md bg-emerald-500" style={{ height: bar() }} />}
</Index>

Use Index when the list length is stable and it's the values at each slot that change (equalizer bars, a fixed set of rows). It never moves nodes — it just updates the value at each position.

Switch / Match

Pick one of several branches — the JSX form of a switch statement:

<Switch fallback={<Text>Idle</Text>}>
  <Match when={state() === "loading"}><Text>Loading…</Text></Match>
  <Match when={state() === "ready"}><Text>Ready.</Text></Match>
</Switch>

The first Match whose when is truthy renders; if none match, fallback renders.

App-shell primitives

@pocketjs/core/components also exports a layer of higher-level primitives that compose View with focus and overlay behavior:

Primitive Purpose
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 over the overlay.
ActionBar Docked bottom bar in the overlay layer.

These are documented in full — with focus semantics and examples — on the App shell and Input & focus pages. The complete typed signatures live in the API reference.