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 primitives — View, 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.