Predictive Grid Morph
48 HTML cells in an 8 × 6 grid that, within 120 px of the mouse, move away from it by up to 15 px, grow up to × 1.3 and brighten, then settle back in 0.4 s when it moves on: a raised magnifier that follows the pointer across the grid. CSS Grid and 1.8 KB of JavaScript, no canvas; color only when the user reduces motion. The grid reacts to the mouse's position, it does not predict its path.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Liquid has 31 effects, including 6 free. Explore the category →
3 usage examples



How it works
The .fx-0417-root root (at least 300 px tall, #0a0a0f background) holds .ai-grid-morph, a CSS grid of 8 columns and 6 rows in 1fr, a 4 px gap, 16 px of padding. The script creates the 48 .ai-grid-cell cells inside it: indigo background at 30%, 4 px corners. In a 640 × 300 px frame, a cell is about 72 × 41 px. Since the cells are real HTML elements, you can put text, an image or a pattern in them, as the examples do.
On each mousemove over the grid, the script measures the distance from the mouse to the center of each cell. Under 120 px, it computes a force from 1 (cell under the mouse) to 0 (cell 120 px away), then writes inline styles: an offset away from the mouse (force × 15 px), a scale-up (1 + force × 0.3) and a more opaque background (30% + force × 50%). Beyond 120 px, or when the mouse leaves the grid (mouseleave), the inline style is removed. The return, like every change, goes through the cell's CSS transition: transform .4s cubic-bezier(.23, 1, .32, 1), quick at first and settling gently, and background .3s.
The result reads like a raised magnifier: nearby cells lift and move apart to clear the targeted one. The script keeps no memory of previous positions: the shape of the distortion depends only on where the mouse is at that moment. No animation loop runs: nothing is computed while the mouse is not moving over the grid (measured: 0% of pixels changed at rest).
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, the script no longer writes any offset or scale, only the color, and the:hoverrule loses its zoom. Measured in Chromium 151, Firefox 153 and WebKit 26: no cell moves, those under the mouse brighten (12.5% of pixels change, against 14.5% with motion). - Keyboard: the cells are
divs with no role or focus; the grid only reacts to the mouse. If each cell is an action (picking a cell, a pad, a tile), usebuttons and repeat the highlight on focus:.ai-grid-cell:focus-visible { background: rgba(99, 102, 241, .8) }. - Touch: with no hover, a tap sends a single
mousemoveat the touched point; measured in Chromium and WebKit in touch mode, 21 to 22 cells stay transformed around the point until the next tap, since nomouseleavefires. Provide a clear selection on tap if the grid is used to choose. - Screen readers: the 48 empty cells say nothing. Put each cell's content in text, like the vegetable initials or instrument names in the examples.
Browser compatibility
CSS Grid, transitions and 1.8 KB of JavaScript, no dependency, no canvas. Measured in Chromium 151, Firefox 153 and WebKit 26: same radius, same offsets, same return when the mouse leaves, color only under reduced motion. The script queries the position of the 48 cells (getBoundingClientRect) on every mouse move: no noticeable cost at 48 cells, worth watching beyond a few hundred.
Without JavaScript, no cell is created: the root stays an empty dark frame. If the grid carries content, write the cells in the HTML and remove the loop that creates them; the script will find them by their class.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0417-root">
<div class="ai-grid-morph">
<!-- 48 × <div class="ai-grid-cell"></div> created by the script -->
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Grid size (JS — i < 48; CSS — repeat(8, 1fr) / repeat(6, 1fr)) |
8 × 6 = 48 cells | For 12 × 8, put 96 in the loop and repeat(12, 1fr) / repeat(8, 1fr) in the grid. The cell count must equal columns × rows. |
Reach (JS — const maxDist = 120) |
120 px | 80 px for a tight magnifier that only touches direct neighbors, 200 px for a wide wave. |
Offset (JS — force * 15, twice) |
up to 15 px | 0: cells grow in place without moving apart; 25: a blast effect. |
Scale-up (JS — force * 0.3) |
up to × 1.3 | 0.1 for subtle relief, 0.5 for a bold magnifier. Mind the offset: without it, enlarged cells overlap. |
Color (JS — rgba(99, 102, 241, …); CSS — .ai-grid-cell) |
indigo, 30 to 80% opacity | Change the first three numbers in the script and in both CSS rules. For one color per cell, read it from the cell (cell.dataset.rgb), as in the first two examples. |
Smoothness (CSS — transition: transform .4s cubic-bezier(.23, 1, .32, 1)) |
0.4 s, quick start and soft landing | 0.2 s sticks to the gesture, 0.8 s leaves a wake behind the mouse. |
FAQ
mx + (mx - prevX) * 4 instead of mx (and likewise for my); the magnifier then sits ahead of the mouse in the direction of travel.pointermove instead of mousemove, add touch-action: none on the grid and pointerleave for the return; the page will no longer scroll when the finger slides over the grid.