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 has two ways to move things: declarative motion utilities — Tailwind-subset classes (transition, duration-N, ease-*, delay-N) that tween a node whenever its style is swapped — and the imperative APIanimate(), spring(), and cancelAnim() from @pocketjs/framework/animation, for one-off tweens you kick off from code. Both compile down to the same native machinery. You declare motion once in JavaScript; the Rust core owns the tween from there.

How Native Animation Works

Tweens and springs tick in the Rust core once per vblank at a fixed dt = 1/60 s. That has two consequences worth internalizing:
  • Zero steady-state JS. After you call animate() (or a style swap starts a transition), JavaScript is not involved again until the tween ends. A 20-second drift costs exactly one FFI call to start — no per-frame requestAnimationFrame, no signal churn.
  • Deterministic, byte-exact. Because dt is fixed and frame content is a pure function of the frame index, the same app produces the same pixels on every run. This is what makes PocketJS’s byte-exact PNG goldens possible.
The fixed dt = 1/60 s comes from FIXED_DT in spec/spec.ts and is shared by both the TypeScript runtime and the Rust core — neither side can drift from the other.

Prefer Transforms Over Layout Props

Transform props — translateX, translateY, scale, rotate — never trigger relayout. Color and opacity changes do not trigger it either. Layout props (width, height, padding, margin, inset) relayout the frame they change on, which costs a Taffy pass every animated frame. For anything that runs continuously or on interaction — enters, lifts, focus emphasis, ambient drift — animate a transform and leave layout alone. Reserve layout-prop animation for deliberate one-shot flourishes (like the hero demo’s underline sweep-in).

Declarative Motion: Tailwind Classes

Add motion utilities to a class and the node tweens whenever its style record is swapped — on focus: / active: variant changes (switched natively, zero JS) and when a dynamic class ternary swaps one full literal for another. The core tweens only the animatable props that actually changed between the old and new style.
UtilityAnimates
transitionTransforms + colors + opacity (the default property set)
transition-transformtranslateX/Y, scale, scaleX/Y, rotate
transition-colorsbgColor, gradFrom, gradTo, borderColor, textColor
transition-opacityopacity
transition-allEvery animatable prop (including layout — can relayout)
Tune the tween with:
  • duration-N — duration in ms (duration-150 = 150 ms). Default 150.
  • delay-N — delay in ms. Default 0.
  • ease-linear | ease-in | ease-out | ease-in-out | ease-spring | ease-out-back. Default ease-in-out.
This hero demo button fades its background natively on focus and press — no JS runs on the focus change at all:
import { View, Text } from "@pocketjs/framework/components";
import { createSignal } from "solid-js";

function HeroButton() {
  const [count, setCount] = createSignal(0);
  return (
    <View
      class="px-4 py-2 rounded-xl shadow-md bg-blue-600 border-blue-500 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>
  );
}
A card that lifts and brightens when focused — a translate plus color change, both tweened by one transition-all:
<View
  class="p-3 rounded-xl bg-white border-slate-200 translate-y-1
         focus:bg-blue-50 focus:border-blue-500 focus:translate-y-0
         transition-all duration-150 ease-out"
  focusable
>
  {/* … */}
</View>

Imperative: animate()

import { animate } from "@pocketjs/framework/animation";

animate(node, prop, to, { dur, easing, delay }): number
animate tweens one prop from its current value to to, and returns an animId you can later pass to cancelAnim(). node is a NodeMirror (from a ref) or a raw node id.

Options

OptionTypeDefaultNotes
durnumber (ms)200Ignored by spring easings — those run on physics.
easingEasingName | number"out"A name from the table below, or a raw ordinal.
delaynumber (ms)0Wait before the tween starts.
Only animatable props are accepted. Passing an unknown or non-animatable prop throws at the call site.

Easing Names

NameFeel
"linear"Constant speed.
"in"Ease-in (accelerate).
"out"Ease-out (decelerate). Default.
"in-out"Ease-in-out.
"out-back"Overshoots the target, then settles (~10%).
"spring"Physics spring; dur is ignored.
"spring-bouncy"Springier spring with more overshoot; dur ignored.

Animatable Props

Only these prop names are accepted by animate() and spring():
Prop familyUnits
translateX, translateYPixels
width, height, padding, margin, inset propsPixels
scale, scaleX, scaleYMultiplier (1.0 = 100%)
rotateDegrees
opacity01
bgColor, gradFrom, gradTo, borderColor, textColoru32 ABGR or hex string

Getting a Node Ref

Give any component a ref and Solid assigns the underlying NodeMirror to your variable. Kick the tween off in onMount, once the node exists. This is the hero demo’s title underline:
import { View, type NodeMirror } from "@pocketjs/framework/components";
import { animate } from "@pocketjs/framework/animation";
import { onMount } from "solid-js";

function Underline() {
  let el: NodeMirror | undefined;
  onMount(() => {
    // Sweeps from w-0 to 210px once on mount — native tween, zero steady-state JS.
    if (el) animate(el, "width", 210, { dur: 700, easing: "out", delay: 150 });
  });
  return <View ref={el} class="h-1 w-0 rounded-full shadow bg-gradient-to-r from-blue-500 to-cyan-500" />;
}
width is a layout prop, so this relayouts each frame while tweening — fine for a one-shot mount flourish, but use transforms for hot paths.

Animating Colors

Color props tween per ABGR channel natively. Pass a packed u32 ABGR value or a '#rrggbb' / '#rrggbbaa' hex string as to:
import { animate } from "@pocketjs/framework/animation";

animate(card, "bgColor", "#3b82f6", { dur: 150 });

Imperative: spring()

import { spring } from "@pocketjs/framework/animation";

spring(node, prop, to, preset?): number
spring tweens to to with a physics spring — the duration comes from the physics, not a timer, so there is no dur option. preset is "default" (moderate spring) or "bouncy" (more overshoot). It returns an animId like animate. The canonical enter pattern: set the start value with a style={{ … }} object and animate to the end value on mount.
import { View, Text, type NodeMirror } from "@pocketjs/framework/components";
import { spring } from "@pocketjs/framework/animation";
import { onMount } from "solid-js";

function Detail(props: { title: string; detail: string }) {
  let el: NodeMirror | undefined;
  onMount(() => {
    if (el) spring(el, "translateY", 0); // springs up from +22px starting position
  });
  return (
    <View ref={el} style={{ translateY: 22 }} class="p-3 rounded-xl bg-white">
      <Text class="text-sm text-slate-950 font-bold">{props.title}</Text>
      <Text class="text-xs text-slate-600">{props.detail}</Text>
    </View>
  );
}

cancelAnim()

Stop a running tween with the id animate() or spring() returned:
import { animate, cancelAnim } from "@pocketjs/framework/animation";

const id = animate(streak, "translateX", 300, { dur: 20000, easing: "linear" });
// … later:
cancelAnim(id);
You rarely need this for one-shots — destroying a node frees its animation tracks automatically.
The Gallery component drives its page slide with a single animate() call on each page change. The inner strip’s translateX is animated to -page * SCREEN_W. Because translateX is paint-only, no relayout occurs and the cost is one FFI call per LTRIGGER/RTRIGGER press:
// From src/components.ts — Gallery internals
createEffect(() => {
  const p = props.page();
  if (!strip || p === prevPage) return;
  prevPage = p;
  animate(strip, "translateX", -p * SCREEN_W, { dur, easing });
});
Two long-running ambient tweens (from the cards demo) buy 20+ seconds of motion with zero further JS after mount:
onMount(() => {
  if (streakA) animate(streakA, "translateX", 300, { dur: 20000, easing: "linear" });
  if (streakB) animate(streakB, "translateX", -260, { dur: 26000, easing: "linear" });
});

Layout-Prop vs. Transform Animation

Layout props (width, height, padding, margin, inset) trigger a Taffy relayout pass every frame they are animating. Use them only for deliberate, infrequent one-shots like a drawer opening or a bar growing.Transform props (translateX, translateY, scale, rotate) and paint props (opacity, color props) never trigger relayout — prefer them for all interactive and continuous motion.
The toggle knob in the settings demo uses animate(knob, "translateX", x) instead of changing width or left, so the slide is paint-only:
import { View, type NodeMirror } from "@pocketjs/framework/components";
import { animate } from "@pocketjs/framework/animation";
import { createEffect } from "solid-js";

function ToggleKnob(props: { on: boolean }) {
  let knob: NodeMirror | undefined;
  let initialized = false;
  createEffect(() => {
    if (!knob) return;
    const x = props.on ? 15.5 : 0.5;
    animate(knob, "translateX", x, { dur: initialized ? 160 : 1, easing: "out" });
    initialized = true;
  });
  return (
    <View ref={knob} class="w-4 h-4 rounded-full bg-white border-indigo-200 shadow-md m-[2]" />
  );
}

Build docs developers (and LLMs) love