near-field 0.2: Cursor Proximity Without React
near-field 0.2 ships a vanilla entry: the same proximity field, springs, and Magnetic, Tilt, and Glow effects with no framework at all. Plain functions that take an element and return a cleanup, about 3KB min+gzip, and importable straight from a CDN with no bundler.
When I shipped near-field in July, the last paragraph said the field and springs were framework-free internally, so "vanilla or Vue adapters stay possible later". Version 0.2.0 makes the first half of that true. There is now a near-field/vanilla entry with the same primitive and the same three effects, and React never enters the bundle.
If you haven't seen it before: near-field turns cursor proximity into a CSS input. An element carries --nf (0 far away to 1 touching), plus --nf-x and --nf-y (a unit vector toward the pointer), and your stylesheet decides what "near" means. The demo site shows it better than I can describe it.
The vanilla API
Every function takes an element you already have and returns a cleanup:
import { nearField, magnetic, tilt, glow } from "near-field/vanilla";
// the primitive: same options as useNearField
const stop = nearField(document.querySelector(".card"), { radius: 140 });
// the effects: attach to the element, no wrapper
magnetic(document.querySelector(".cta"), { strength: 0.3 });
tilt(document.querySelector(".cover"), { max: 6 });
glow(document.querySelector(".card"), { color: "rgba(120, 170, 255, 0.3)" });
// later
stop();nearField takes the same radius, onFrame, and cssVars options as the hook, so the CSS from the first post works unchanged:
.card {
transform: translate(
calc(var(--nf-x) * var(--nf) * 8px),
calc(var(--nf-y) * var(--nf) * 8px)
);
}The effects take the same options as their React components, minus as. The components render a wrapper element; the vanilla functions can't assume they own your markup, so they attach to the element you pass instead. Glow is the one that touches the DOM: it appends a single aria-hidden overlay span inside your element (and sets position: relative if the element was static), because a radial gradient needs somewhere to live.
Cleanups that actually clean up
This was the part I cared about most. In React, unmounting takes care of teardown for you. In plain JavaScript, a library that sets inline styles and walks away leaves you with elements stuck mid-animation.
So every cleanup unregisters the element from the field, stops its springs, removes its listeners, and restores the inline styles it found. Magnetic and Tilt put back whatever transform and will-change you had before. Glow removes its overlay and restores position. nearField removes the three custom properties. I checked it in a browser the boring way: attach everything, detach everything, and confirm every element's style attribute is back to what it was.
That makes it safe for the places vanilla code actually runs: pages that swap content without a full reload, htmx fragments, web components with connectedCallback and disconnectedCallback, or a CMS page where elements come and go.
No bundler required
"Vanilla" should mean an HTML file, a CSS file, and a JS file. It does. The build only uses relative imports internally, so a browser can load it directly from a CDN:
<script type="module">
import {
magnetic,
glow,
} from "https://cdn.jsdelivr.net/npm/near-field@0.2/dist/vanilla.js";
magnetic(document.querySelector(".cta"));
glow(document.querySelector(".card"));
</script>No install, no build step. The browser fetches vanilla.js and the one shared chunk it imports, and that's it.
If you'd rather keep the near-field/vanilla name in your code (or point it at a local node_modules copy), an import map does what a bundler would have done:
<script type="importmap">
{
"imports": {
"near-field/vanilla": "https://cdn.jsdelivr.net/npm/near-field@0.2/dist/vanilla.js"
}
}
</script>The only catch is a general one: browsers won't load ES modules from a page opened as file://, so serve the folder with anything static (python3 -m http.server is enough) while you develop.
The refactor was one file
The reason this could ship as a minor version is that the architecture from 0.1 held up. The pointer field (one passive pointermove listener and one shared animation frame loop for every subscriber) and the spring integrator never imported React in the first place. The React hook was always a thin adapter over them, and the vanilla functions are a second thin adapter over the same core.
The one thing that had to move was reduced-motion detection. It lived in a file that imported useSyncExternalStore, which would have dragged React into the vanilla bundle. Splitting it into a framework-free prefersReducedMotion plus subscribeReducedMotion, with the React hook in its own file on top, was the entire refactor. React is now an optional peer dependency, so a vanilla install pulls in nothing.
Everything else carries over for free: the fixed 120Hz spring substep, the edge-based distance with smoothstep easing, Magnetic and Tilt collapsing to rest under prefers-reduced-motion, and the tuned press states on touch screens where there is no "near".
Sizes
The vanilla entry with all three effects is about 3KB min+gzip. The React entry with its three components is about 3.5KB. Still no dependencies.
For anyone building an adapter of their own, the vanilla entry also re-exports the framework-free core: registerElement, computeFrame, createSpring, springPresets, prefersReducedMotion, and subscribeReducedMotion.
Try it
npm install near-fieldThe demo site has a new vanilla tab under Use, the lab is still the best place to feel the presets, and the package is on npm. Next on the roadmap: a Vue adapter, scroll proximity, and gyroscope tilt on mobile.
If you drop it into a plain HTML page and it does something nice, send it my way.
Have a wonderful day.