89 / Scenes
Lattice Field
A grid of dots that parts around the pointer and falls apart when pressed, then climbs back into line.
Press it and watch it drop
The grid parts around your pointer. Press once to break it, press again to let it climb back.
"use client"
import { LatticeField } from "mischief-ui/lattice-field"
export function LatticeFieldDemo() {
return (
<LatticeField className="border-border bg-card w-full max-w-xl rounded-[var(--radius)] border">
<div className="px-8 py-16 text-center">
<h3 className="text-2xl font-semibold">Press it and watch it drop</h3>
<p className="text-muted-foreground mt-2 text-sm">
The grid parts around your pointer. Press once to break it, press
again to let it climb back.
</p>
</div>
</LatticeField>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/lattice-fieldimport { LatticeField } from "mischief-ui/lattice-field"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { RenderSurface, useThemeColors, type SurfaceColor,} from "@/registry/default/render-surface/render-surface"import { cn } from "@/lib/utils" export type LatticeFieldProps = React.HTMLAttributes<HTMLDivElement> & { /** Pixels between dots. The lattice is rebuilt to suit whatever box it gets. */ spacing?: number /** Diameter of a dot in pixels. */Usage
export function Backdrop() {
return (
<LatticeField className="rounded-xl border">
<div className="px-8 py-16 text-center">
<h2>Press it</h2>
</div>
</LatticeField>
)
}Nothing is simulated
A dot has no stored position. Where it lands is worked out from its place in the grid, the clock, and the handful of numbers the pointer contributes, so a frame is one draw call over one buffer that was filled when the lattice was built. There is no velocity to integrate and no per-dot state to keep in step.
The fall is ballistic rather than physical: an outward kick that fades with distance from the press, gravity on top of it, and a floor each dot is not allowed past, set a little differently for every dot so the pile is uneven. It costs the same as the resting grid, and letting go is a mix back towards the grid rather than a simulation that has to be unwound.
The same lattice, drawn twice
Each frame draws the buffer twice. The first pass runs the same shader with the motion turned down and paints in echoColor, so it sits behind the real dots as a version of the grid that has not caught up yet. The depth comes from the two passes disagreeing, not from a second set of dots being tracked.
Spacing, not count
A lattice is described by the gap between its dots, so it looks the same in a narrow column as across a wide hero and needs no adjusting when the box changes. Past a budget the spacing widens on its own rather than the count climbing, which keeps a full page backdrop from asking the GPU for hundreds of thousands of points.
The pointer is followed on the window rather than on this element, so the lattice still answers to it while sitting behind a headline that is taking every event itself. Setting pointerRadius to zero removes the reaction, and collapseOnClick to false leaves the grid unbreakable.
API
spacingnumberPixels between dots. The lattice is rebuilt to suit whatever box it gets. Defaults to 16.dotSizenumberDiameter of a dot in pixels. Defaults to 2.colorstringA theme custom property or a CSS colour for the dots. Defaults to "--foreground".echoColorstringThe lagging second colour drawn behind. Setting it to the same value as color turns the echo off. Defaults to "--primary".pointerRadiusnumberHow far the pointer reaches, in pixels. Zero turns the reaction off. Defaults to 140.pushnumberFurthest a dot is pushed by the pointer, in pixels. Defaults to 26.gravitynumberPixels per second squared, once the lattice has been let go. Defaults to 1400.scatternumberPixels per second the dots leave the press at. Defaults to 320.collapseOnClickbooleanWhether a press breaks the lattice. Defaults to true.swaynumberMultiplies the idle wave. Zero holds the lattice perfectly still. Defaults to 1.pausedbooleanHolds the lattice still.Accessibility
Decoration, hidden from assistive technology, and behind its children on its own layer, so a press meant for it reaches whatever is on top instead. Under reduced motion the lattice is painted once at rest: the wave never starts and the collapse never runs, so the pattern is there without any movement. Nothing is said only by the collapse, which is why there is no keyboard route to it.