PocketJS has two ways to move things: declarative motion utilities — Tailwind-subset classes (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.
transition, duration-N, ease-*, delay-N) that tween a node whenever its style is swapped — and the imperative API — animate(), 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 fixeddt = 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-framerequestAnimationFrame, no signal churn. - Deterministic, byte-exact. Because
dtis 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 aclass 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.
| Utility | Animates |
|---|---|
transition | Transforms + colors + opacity (the default property set) |
transition-transform | translateX/Y, scale, scaleX/Y, rotate |
transition-colors | bgColor, gradFrom, gradTo, borderColor, textColor |
transition-opacity | opacity |
transition-all | Every animatable prop (including layout — can relayout) |
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.
hero demo button fades its background natively on focus and press — no JS runs on the focus change at all:
transition-all:
Imperative: animate()
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
| Option | Type | Default | Notes |
|---|---|---|---|
dur | number (ms) | 200 | Ignored by spring easings — those run on physics. |
easing | EasingName | number | "out" | A name from the table below, or a raw ordinal. |
delay | number (ms) | 0 | Wait before the tween starts. |
Easing Names
| Name | Feel |
|---|---|
"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 byanimate() and spring():
| Prop family | Units |
|---|---|
translateX, translateY | Pixels |
width, height, padding, margin, inset props | Pixels |
scale, scaleX, scaleY | Multiplier (1.0 = 100%) |
rotate | Degrees |
opacity | 0–1 |
bgColor, gradFrom, gradTo, borderColor, textColor | u32 ABGR or hex string |
Getting a Node Ref
Give any component aref 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:
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 packedu32 ABGR value or a '#rrggbb' / '#rrggbbaa' hex string as to:
Imperative: spring()
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.
cancelAnim()
Stop a running tween with the id animate() or spring() returned:
Ambient Animation: The Gallery Page Slide
TheGallery 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:
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.animate(knob, "translateX", x) instead of changing width or left, so the slide is paint-only: