Cookbook

Framework interop

The engine is a browser runtime with a server-safe authoring surface. That distinction is the whole topic: because a body is declared in attributes, it survives serialization and arrives in the HTML the server sent — and the runtime, whenever it attaches, simply adopts what it finds.

Shipped · this site is the worked example
There is no SSR API, and there should not be. Nothing renders a field on the server: there is no canvas, no rAF and no viewport there. What survives the server is the declaration — and that is enough, because the field's contract is markup.

Server rendering

A server-rendered page is complete before any script runs: the content is there, the semantics are there, and the bodies are there as attributes. When the runtime attaches it scans and begins measuring. Nothing about the elements changes — they were already bodies; they just weren't being measured yet.

HTML
<!-- Server-rendered, and complete without JavaScript. The contract is
     ATTRIBUTES, so it serializes: the body exists in the HTML the server sent. -->
<article data-body="attract" data-feedback data-hot>
  <h2>Quarterly report</h2>
</article>

<!-- The runtime attaches later and adopts what it finds. Nothing about the
     element changes on hydration — it was already a body, it just wasn't measured yet. -->
<field-root></field-root>

This is also why render: 'none' is such a natural default for content sites: the no-JavaScript page and the hydrated page differ by a measurement, not by a layout.

Islands and hydration

An island hydrates after the field's initial scan, so bodies it renders are new to the engine. The rule is small and easy to get wrong in exactly one direction:

  • New elements → scan(). The field does not poll the DOM; tell it when the set of bodies changed.
  • Changed parameters → nothing. A body whose strength or range changes is picked up on the measure cadence. Use handle.set(...) for a programmatic body; re-scanning for this is waste.
  • One field per page. <field-root> is a page singleton. Two islands that each mount a page-wide field give you two engines writing the same variables — use one field and let islands contribute bodies, or scope each to its own contained field.
// An ISLAND that renders bodies must tell the field they arrived: the engine
// scans at boot, and an island hydrates after that.
useEffect(() => {
  field.scan();        // re-scan for the bodies this island just rendered
}, [items]);

// A body whose PARAMETERS change does not need a rescan — the reactive-param path
// applies within a frame:
handle.set({ strength: next });

JS ✓ Swift ✓ Kotlin ✓ after the DOM changes

Teardown is not optional

In a single-page app this is the bug that actually bites. A field owns a requestAnimationFrame loop, window listeners and — when contained — a ResizeObserver. A component that mounts a field and never destroys it leaks an engine per navigation, and the symptom is a page that grows steadily heavier rather than one that breaks.

React
// SPA navigation is where fields leak. A field owns a rAF loop, listeners and
// (contained) a ResizeObserver. Tear it down on unmount, every time.
useEffect(() => {
  const field = createField(canvas, { host: browserHost() });
  return () => field.destroy();
}, []);

This site, as the worked example

These docs are a static Astro build with client-side routing. The field is a persisted island: a single <field-root> in the base layout, outside the swapped content, so navigating between pages changes the document without restarting the engine — the field stays continuous across the transition.

Astro layout
---
// This site: a static Astro build. The field is a persisted island — one <field-root>
// in the base layout, outside the swapped content, so client-side navigation changes
// the page WITHOUT restarting the engine.
---
<field-root render="none"></field-root>
<slot />

Page-level runtimes follow the same discipline: each boots on load and on every navigation, and tears down before the swap, so nothing stacks. Every live demo in this cookbook is wired that way.