A reactive component — no particles
The field is a behaviour layer, not a particle background. Here it runs contained inside one list and signals-only — nothing is drawn. The only thing you see is an ordinary list whose rows gain weight, lift, and glow from the field, and respond when you engage them. This is what you'd actually build.
- Production incident — checkout 500s
- Design review: onboarding flow
- PR #318 — signals-first default
- Weekly metrics digest
- Renew TLS certificate
- Coffee chat with Sam
--field-density; hover or focus a row and the field gathers toward it.
Hover or tab through the rows. Each is a [data-body]; the contained field gathers its
invisible matter toward them and writes the local density back as --d
(the field-namespaced --field-density holds the same value). Important rows are visibly
heavier; engaging one pulls the field toward it.
There is no <canvas> on this page — the field is pure signal.
Copy it into your app
Three parts: the markup, the field, and the CSS that reads what the field writes.
<ul class="inbox" data-reactive-list>
<li data-body="attract" data-feedback data-strength="1.0" data-range="180">Production incident</li>
<li data-body="attract" data-feedback data-strength="0.62" data-range="180">Design review</li>
<li data-body="attract" data-feedback data-strength="0.30" data-range="180">Weekly digest</li>
</ul> import { FieldField } from '@fundamental-engine/vanilla';
const list = document.querySelector('[data-reactive-list]');
// A CONTAINED, signals-only field: it runs the full simulation over the rows but
// draws nothing (render: 'none'). The only output is the --field-* CSS variables.
const field = new FieldField({ bounds: list, render: 'none', density: 1.4 });
// engagement is just an attribute the field reads — hover/focus a row and it
// gathers the (invisible) matter toward itself; neighbours feel the shift.
for (const row of list.querySelectorAll('li')) {
row.addEventListener('pointerenter', () => row.dataset.active = '1');
row.addEventListener('pointerleave', () => row.dataset.active = '0');
} /* the field wrote --d (the canonical raw density channel; --field-density is
its field-namespaced form) onto each [data-feedback] row.
read it to turn density into presence — weight, lift, glow. No canvas involved. */
.inbox li {
font-weight: calc(400 + var(--d, 0) * 360);
transform: translateX(calc(var(--d, 0) * 6px));
box-shadow: 0 0 calc(var(--d, 0) * 26px) rgba(77, 163, 255, calc(var(--d, 0) * 0.45));
transition: all 0.45s ease;
} render: 'none' is the default since #538 —
a field draws nothing until you ask it to. bounds scopes the field to one element
instead of the window. Together they make the field a component, not a backdrop.