Cover Wall
A React grid wall of covers that fills in cell by cell from the centre outward, with a rolling counter and copy overlaid in the middle; hovering pushes a cell forward while neighbours recede.
Lay out a whole wall of book covers, album art or thumbnails as a grid that fades in cell by cell when it enters the viewport — each cell goes from blurred, slightly small and transparent to sharp and in place. The fill order can run from the centre outward, row by row, or as a deterministic shuffle; a radial scrim sits over the centre of the grid carrying a digit-rolling counter, a title and a description. Hovering a cell pushes it forward with a deeper shadow while its eight neighbours recede slightly; pass onSelect and every cell becomes a clickable button.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add cover-wall.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<CoverWall/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/cover-wall.en.jsonOr, once registries are configured in components.json, install it as @webberui/cover-wall.
Usage
import { CoverWall } from "@/components/ui/cover-wall";
<CoverWall
images={covers} // { src: string; alt: string }[]
columns={6}
order="center-out"
counter={500}
counterSuffix="+"
title="New This Month"
description="books and counting"
onSelect={(index) => console.log(covers[index].alt)}
/>Without images, you can pass any cell content as children:
<CoverWall columns={5} aspect="1/1">
{albums.map((album) => (
<AlbumTile key={album.id} album={album} />
))}
</CoverWall>Props
| Prop | Type | Default | Description |
|---|---|---|---|
images | CoverWallImage[] | — | Cover images; when omitted, children are used, one child per cell |
children | React.ReactNode | — | Arbitrary cell content (one child per cell); ignored when images is set |
columns | number | 6 | Column count; capped at 4 below 640px viewport width |
aspect | string | "3/4" | Cell aspect ratio (CSS aspect-ratio), reserving space before images load |
gap | number | 6 | Gap between cells (px) |
order | "center-out" | "row" | "random" | "center-out" | Fill order: centre outward, row by row, or deterministic shuffle |
stagger | number | 28 | Delay between cells (ms); when omitted, reads --wb-cw-stagger |
counter | number | null | null | Counter overlaid in the centre; hidden when null |
counterSuffix | string | "+" | Suffix after the counter |
title | React.ReactNode | — | Centre title |
description | React.ReactNode | — | Centre description |
onSelect | (index: number) => void | — | Called when a cell is clicked (or activated with Enter / Space), with the cell index; cells become buttons when set |
className | string | — | Passed through to the root container |
CoverWallImage
| Field | Type | Default | Description |
|---|---|---|---|
src | string | — | Image URL (your own image; the component ships no external images) |
alt | string | — | Alt text; also used as the button aria-label when cells are clickable |
How it works
- Fill: once the grid enters the viewport (IntersectionObserver, fires once), each cell waits
rank × staggerand transitions over 420ms fromopacity 0,scale(0.9)andblur(6px)into place. - Order:
center-outsorts by Euclidean distance from each cell to the grid centre;rowruns from the top-left row by row;randomshuffles with a deterministic hash of the index, so SSR and the browser agree. When narrow screens drop to 4 columns, the order and neighbours are recomputed from the real column count. - Counter: each digit is a 0–9 column wheel that springs (stiffness 120, damping 20) to its target, starting at about 60% of the fill.
- Images load with
loading="lazy",decoding="async"andobject-cover, and the cell'saspect-ratioreserves the space up front, so nothing shifts. - Overridable CSS variables:
--wb-cw-stagger(28ms),--wb-cw-fill-duration(420ms),--wb-cw-hover-duration(200ms),--wb-cw-ease(cubic-bezier(0.23,1,0.32,1)),--wb-cw-scrim(centre scrim colour, neutral-950 at 70% by default). - The hover push-in only applies to fine pointers (mouse, trackpad); taps on touch devices do not scale.
Accessibility
- Cells are a
ul/lilist; withonSelecteach cell is atype="button", reachable with Tab, activated with Enter / Space, and shows a focus-visible ring - With
imagesand clickable cells, the button uses the imagealtas itsaria-label; non-clickable images keep theiralt - The centre overlay ignores pointers; the counter region is
aria-live="off", screen readers get only the full value, and rolling digits are hidden witharia-hidden - When the user has enabled reduce motion, all cells appear immediately, the counter shows its final value, and hover only deepens the shadow without scaling or receding neighbours
Device Stage 3D
A pure-CSS 3D stage for phones, tablets and laptops with five arrangements: pointer tilt, scroll orbit, Y-axis flip, a fanned lineup of devices and a scroll-driven macro pull-back; screens are live DOM.
Image Strobe
A React strobe effect that flips full-bleed images at high speed while centred content stays fixed, stopping on the last frame; triggered on entering the viewport, on mount or via ref.play(), with a slow crossfade under reduced motion.