Resources

Troubleshooting

The handful of things that trip people up — each with the why and the fix. Most reduce to one of two facts: the field reads bodies on a schedule, and it refuses to fake motion.

New elements aren't reacting

The field scans for [data-body] elements when it mounts. If you add or remove bodies afterwards — a route change, a list update, a modal — it won't see them until you re-scan. Call field.scan() (or its alias rescan()) after the DOM settles.

Rescan
// plain DOM / web component
field.scan();

// React — bodies render after the field mounts, so scan from onReady
<FieldField onReady={(field) => field.scan()} />

The field is frozen or blank

Two behaviours pause the loop on purpose — both are features, not bugs:

  • Reduced motion. Under prefers-reduced-motion: reduce the simulation renders a single static frame (dt = 0) and never animates. Test with the OS setting off to see motion.
  • Backgrounded tab. The loop stops on visibilitychange and resumes when the tab is foregrounded again, so a hidden tab costs nothing.
  • Off-screen cells. A <field-cell> gates its loop on an IntersectionObserver — it only runs while visible.

The canvas covers my content or eats clicks

With <field-root> and the React component this is handled for you. If you own the canvas via createField, position it fixed behind your content and let pointer events pass through:

CSS
/* the canvas is decorative — behind content, never intercepting clicks */
.forces-canvas {
  position: fixed;
  inset: 0;
  z-index: 0;          /* your content sits above */
  pointer-events: none;/* clicks pass through */
}
One field per page. There is exactly one background field. Don't mount several <field-root> elements — for small in-frame effects use <field-cell> instead, which is built to be embedded many times.

--field-density isn't updating my element

Density write-back is opt-in. The element needs data-feedback, and your CSS should default --field-density (via the var(--field-density, 0) fallback) so it renders before the first write. The value is low-pass filtered (~0.2s), so it eases rather than jumps. (--field-density is the field-namespaced form of the canonical raw channel --d; both hold the same value.)

HTML + CSS
<!-- 1. opt the element in -->
<h1 data-body="attract" data-range="320" data-feedback>weighty</h1>

<style>
  h1 {
    /* the engine eases this in ∈ [0,1]; default it yourself so it renders before
       the first write. --d is the canonical raw density channel; --field-density is
       its field-namespaced form. */
    font-variation-settings: 'wght' calc(380 + var(--field-density, 0) * 420);
  }
</style>
Words glow, they are never drawn. The engine never assembles particles into letterforms — text stays real, selectable, and accessible; --field-density drives weight, glow, and scale on the real element.

SSR & hydration

The field is a client-only concern. The custom element registers in the browser, and the React component mounts its canvas on the client — there is no server-rendered canvas to mismatch. In Astro, the import '@fundamental-engine/elements' lives in a client <script>; in Next.js, render the field in a client component. Bodies are plain markup, so they server-render fine; only the field's reaction is deferred to the client.

The field keeps running after I remove it

A field owns real resources — a running loop, event listeners, an idle timer, and (depending on the surface) a canvas, GPU buffers, and registry observers. How they're released depends on how you mounted it:

  • <field-root> / <field-cell> — automatic. The element tears itself down on disconnect (and rebuilds cleanly on an attribute change): the loop stops, the platform registries are pruned, and the visual-binding observers disconnect. Just remove the element.
  • createField() / the Three.js FieldLayer — you hold the handle, so you call destroy() when the view goes away. It stops the loop, releases the host's listeners, clears the idle timer, retires agents, and frees the swarm's GPU buffers. Skipping it on a re-mounted or route-swapped view is the usual cause of a slow leak.
JavaScript
// <field-root> — automatic: it tears down on disconnect / attribute rebuild
el.remove();                 // registries pruned, observers disconnected, timers cleared

// createField / FieldLayer — you own the handle, so you call destroy()
const field = createField(canvas, opts);
// …later, when the view goes away:
field.destroy();             // stops the loop, releases listeners, frees GPU + timers
Long-lived pages stay flat. The registries prune entries for elements that leave the DOM on a low cadence, so a single-page app that mounts and unmounts bodies for hours doesn't accumulate stale state. You don't manage that — it's the runtime's hygiene.

It feels heavy on a large page

The field is one shared canvas, so cost does not grow with the number of bodies beyond the per-body loop term. If a very large or low-power surface needs headroom, lower densitydensity: 0.5 halves the particle count. See Performance for the full picture.

Still stuck? The Lab fires a single particle into any force and checks it against its law — the fastest way to confirm a force behaves as documented.