A React canvas field of dots and lines that makes room around surfaces: it clears around the ones laid on it, bends as they move, and lights up under the pointer. It is decorative, theme aware, and has no UI framework dependency.
Try the interactive demo with presets, live controls, draggable objects, light and dark themes, and a copyable React configuration.
Install from GitHub:
npm install github:angelolibero/surface-fieldReact 18 or 19 is a peer dependency. Git installation runs the package's prepare build, so consumers receive JavaScript and TypeScript declarations without compiling the source themselves.
import { SurfaceField } from "surface-field";
export function Backdrop() {
return <div style={{ position: "relative", minHeight: 500 }}>
<SurfaceField
connected
gap={22}
focusRadius={600}
lineRadius={200}
baseOpacity={0.02}
maxOpacity={0.2}
tint={0}
breathe={0}
wander={false}
cursorPush={2}
ripplePush={6}
style={{ position: "absolute", inset: 0, color: "var(--foreground)" }}
/>
<div style={{ position: "relative" }}>Your content</div>
</div>;
}The parent supplies size and background. The field draws no background and never intercepts pointer input. Its root is aria-hidden. The pointer light follows the cursor; a press creates a ripple. It pauses while the document is hidden and renders a still frame for reduced motion.
An editor can send object rectangles directly to one field instance. Scene rectangles use CSS pixels relative to a root element's border box. Gesture footprints and previews use viewport CSS rectangles. The example assumes an existing workspaceRef and workspaceElement in the host editor; the copied demo configuration is a backdrop component and does not automatically discover DOM objects.
import { SurfaceField, createSurfaceFieldController } from "surface-field";
const controller = createSurfaceFieldController();
<SurfaceField controller={controller} interactionRoot={workspaceRef} />;
controller.setScene({
root: workspaceElement,
rects: [{ id: "item-1", parent: null, left: 12, top: 20, right: 112, bottom: 90 }],
});Create one controller per mounted field. Keep its identity stable across renders, for example with useMemo(createSurfaceFieldController, []). The controller updates geometry without a React render. See the API, architecture, and integration guide.
React Flow: use the field as the flow's background with examples/react-flow, which forwards the camera and node boxes to the controller.
To draw off the main thread, pass a factory for the package's worker. The field hands its canvases to it as OffscreenCanvases and falls back to the main thread where that is not available. With Vite:
import FieldWorker from "surface-field/worker?worker";
<SurfaceField worker={() => new FieldWorker()} />;Use surfacePadding to adjust the fade around scene and carried surfaces in CSS pixels. Its default 0 preserves the existing spacing; positive values widen the empty band and negative values narrow it. The surface interior stays clear.
npm ci
npm run typecheck
npm test
npm run build
cd demo && npm ci && npm run devThe demo is a separate Vite app. Its shadcn/ui controls live only in demo/; the library runtime imports React and its own modules only. npm run demo:build builds both package and demo. npm pack includes the built library and docs, not the demo app.
MIT. See LICENSE. Made by Angelo Libero.
