verve.anim

Guide: Animation · live: example.

Builders

ExportNotes
to(alloc, target: ?selector)tween toward given values
from(alloc, target)tween from given values to rendered state
timeline(alloc)sequence/compose tweens
reveal(alloc, class_name, trigger_opts)zero-wasm class toggle on scroll
draggable(alloc, DraggableOpts)pointer drag descriptor (Node.draggable)
sortable(alloc, SortableOpts)drag-to-reorder descriptor (Node.sortable)

Tween chain

Properties: opacity x y scale scaleX scaleY rotate + propFrom(name, v) for arbitrary transforms. Timing: duration(s) delay(s) ease(Ease) repeat(n|-1) yoyo(bool). Keyframes: step(pct) + stepEase(Ease). Multi-target: stagger(.{ .each, .from }). Accessibility: reducedMotion(.respect|.skip). Specials: motionPath(.{ .path, .rotate }), morph(.{ .from, .to }), onComplete callbacks (island side).

Ease: linear, out_cubic, in_out_sine, out_back, elastic and bounce families, etc.

ScrollTrigger

.scrollTrigger(.{
    .start = .{ .viewport = .{ .pct = 85 } },           // or .{ .trigger = .top, .viewport = .top }
    .end = .{ .rel_vh = 1.5 },                          // or absolute trigger/viewport specs
    .actions = .{ .on_enter = .play, .on_leave_back = .reverse },
    .scrub = .exact,                                    // or .{ .smooth = 0.3 }
    .pin = .self,
    .snap = .{ .step = 1.0/3.0 },                       // or .{ .points = &.{…} }
    .snap_ease = .out_cubic,                            // any Ease; default .out_cubic
    .snap_directional = false,                          // true = bias toward travel direction
    .scroller = null,                                   // CSS selector; wire "sl"
    .scroller_handle = null,                            // island ref-handle; wire "slh"; wins over .scroller
    .markers = true,
    .once = true,
})

scroller / scroller_handle: compute trigger geometry against a scrollable container instead of the window. v1: snap stays window-scoped; pin uses position:fixed — neither composes with container scrollers yet.

snap_ease: override the snap glide ease curve. snap_directional: when true, snaps to the nearest target in the direction of scroll travel (falls back to nearest if no target in that direction).

SplitText

Node.splitText(opts) — split a text node into animatable spans server-side.

OptionTypeDefaultNotes
byBy.chars.chars · .graphemes · .words · .words_and_chars · .lines
rtl_awareboolfalseWrap RTL runs in <span dir="rtl">
index_attrboolfalseStamp data-st-i on each leaf span

Emitted classes: .st-char (chars/graphemes), .st-word (words), .st-line (lines — browser-grouped at hydrate). CSS requirement: .st-char,.st-word{display:inline-block}.

By.graphemes: keeps extended grapheme clusters whole (emoji families, skin-tone modifiers, regional-indicator flags, combining marks) — UAX#29, computed SSR in Zig. By.lines: emits word spans + data-split-lines; bridge groups by offsetTop once at hydrate.

A11y: the split-wrap is aria-hidden; the parent element receives aria-label with the original text.

Node hooks

Node.animate(t) · animateJson(json) · splitText(opts) (see above) · draggable(d) · sortable(s) · smoothScroll(.{ .smooth = 1.2 }) · parallaxSpeed(f) · parallaxLag(f).

Draggable opts

bounds (selector), inertia (.on), snap (.{ .grid = .{x,y} }), toggle_class, axis constraints, drop zones (on_drop island callbacks; .drop-hover class is zero-wasm).

bounce (?f64, default null) — elastic reflection when a throw hits a bound. Value in [0, 1]: 0 = clamp-equivalent; ~0.2 = GSAP-like; 1 = fully elastic. Requires both inertia (non-.off) and bounds (non-.none) — validation errors: BounceWithoutInertia / BounceWithoutBounds. Out-of-range values return BadBounce.

Sortable

Node.sortable(anim.sortable(alloc, opts)) — drag-to-reorder descriptor.

OptionTypeDefaultNotes
items[]const u8CSS selector for sortable children (required)
handle?[]const u8nullGrip sub-selector; null = whole item
axisAxis.y.x / .y / .both
group?[]const u8nullShared name for cross-list transfer
animatebooltrueFLIP sibling shift; wire "an":0 when false
autoscrollbooltrueEdge scroll; wire "as":0 when false
autoscroll_edge_pxf6440Edge band width in px; wire "ase" when not 40
toggle_class?[]const u8nullClass on dragged item
disabledboolfalseStart disabled; enable via verve.sortableEnable()

Island-only callbacks (SSR rejects with CallbackSlotRequiresIsland):

CallbackFiresRequirement
on_reorder_slotAfter settle, with {from, to}
on_enter_group_slotWhen item enters any group container (both source and target)group must be set

on_enter_group_slot without groupGroupCallbackWithoutGroup.

SortableHandle (island-side, after callbacks fire):

const h: verve.SortableHandle = .{ .id = handle_id };
h.lastFrom()       // slot index before move
h.lastTo()         // slot index after move
h.fromContainer()  // source container ref-handle; -1 if same list
h.toContainer()    // target container ref-handle; -1 if same list

Validation errors: SortableNoItems (empty items), GroupCallbackWithoutGroup (on_enter_group_slot without group), BadAutoscrollEdge (autoscroll_edge_px ≤ 0 or non-finite).

Wire root key "so". Respects prefers-reduced-motion (reorder instant, no FLIP). See sortable demo.

Island-side control

animPlay(timeline) + control exports (pause/play/reverse/restart, speed), flipCapture(selector) → DOM mutation → flipPlay(state, opts, callbacks), drag handles with position/velocity reads, dynamic values + fn modifiers per frame, Observer (wheel/touch velocity), and scroll-trigger progress reads. The smoothed scroll position is readable end-to-end (verve_sm_get).