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 exampleServer 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.
<!-- 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
strengthorrangechanges is picked up on the measure cadence. Usehandle.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 }); // React: the component owns the canvas and the overlay surface for you.
import { FieldField } from '@fundamental-engine/react';
export default function Layout({ children }) {
return (
<>
<FieldField accent="#4da3ff" onReady={(f) => f.scan()} />
{children}
</>
);
} 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.
// 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.
---
// 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.