Skip to content
Development

A “magnetic” hover for a bento grid, almost entirely in CSS

A bento grid is a layout of cells in different sizes packed side by side. A nice effect for it: as the pointer moves, an outline is “pulled” along, sliding to the hovered cell and resizing to match it exactly. This used to take JavaScript to measure positions with getBoundingClientRect(). With CSS anchor positioning, the core is down to a few lines.

The whole trick

.bento {
  li:hover article { anchor-name: --a; }
  .magnet {
    position-anchor: --a;
    inset: anchor(inside);
    transition: inset var(--dur) var(--ease);
  }
}

Those are essentially the only lines the effect needs. The rest is decoration: colour, rounded corners, fading out when nothing is hovered.

Why it works

A moving anchor name. Instead of giving one element a fixed anchor-name, give it only to the cell being hovered: li:hover article { anchor-name: --a }. Move to another cell and the name --a moves with you.

The indicator follows that name. .magnet is an absolutely positioned element that declares position-anchor: --a. With inset: anchor(inside), its top edge matches the anchor's top edge, its left edge the anchor's left edge, and the same for the other two. So the indicator always covers the hovered cell exactly, however big or small it is.

Motion. When the anchor changes, the inset values change, and transition: inset turns the jump into a slide. Two variables, --dur and --ease, let you tune the feel.

Example 1: A bento grid

Move the pointer over the cells. This example has no JavaScript at all. When the pointer leaves the grid, the indicator fades out thanks to .bento:has(li:hover).

Example 1. A bento grid with an indicator that follows the hovered cell.

Example 2: Tuning the motion

The same technique on a navigation bar. Drag the slider to change --dur, pick a curve to change --ease. You can try bouncier curves, but a simple cubic-bezier usually feels the cleanest and most pleasant.

Example 2. A navigation bar with the --dur and --ease variables.

Example 3: A JavaScript fallback

For browsers without anchor positioning, use a support check plus one pointer listener on the container, through event delegation: measure the hovered cell and write its position into CSS variables. Turn on the switch to force the example down the JS path and compare.

Example 3. The same bento grid, with a JavaScript fallback.

Things to watch

  • Put .magnet after the grid in the HTML, inside the same container with position: relative, so it can find the anchor.
  • If the grid uses gap, no cell is hovered while the pointer crosses a gap, and the indicator loses its anchor. The examples above space the cells with padding inside each li to avoid this.
  • Firefox does not yet animate positions computed with anchor(). The indicator still jumps to the right place but does not slide. To get motion there you have to animate it in JavaScript yourself, for example with the FLIP technique or like the JS path in example 3.
  • Hover means nothing on a touch screen. For devices without a precise pointer, consider a different design, for instance letting the indicator move between cells as the user scrolls on narrow screens.
  • The examples turn off the slide when the user has prefers-reduced-motion on, and keep only the fade.

Browser support. The technique relies on CSS anchor positioning. Check caniuse.com/css-anchor-positioning before using it in production, and keep the JS path from example 3 for browsers that lack it.