Media

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.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

28
500
6
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
imagesCoverWallImage[]—Cover images; when omitted, children are used, one child per cell
childrenReact.ReactNode—Arbitrary cell content (one child per cell); ignored when images is set
columnsnumber6Column count; capped at 4 below 640px viewport width
aspectstring"3/4"Cell aspect ratio (CSS aspect-ratio), reserving space before images load
gapnumber6Gap between cells (px)
order"center-out" | "row" | "random""center-out"Fill order: centre outward, row by row, or deterministic shuffle
staggernumber28Delay between cells (ms); when omitted, reads --wb-cw-stagger
counternumber | nullnullCounter overlaid in the centre; hidden when null
counterSuffixstring"+"Suffix after the counter
titleReact.ReactNode—Centre title
descriptionReact.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
classNamestring—Passed through to the root container

CoverWallImage

FieldTypeDefaultDescription
srcstring—Image URL (your own image; the component ships no external images)
altstring—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 × stagger and transitions over 420ms from opacity 0, scale(0.9) and blur(6px) into place.
  • Order: center-out sorts by Euclidean distance from each cell to the grid centre; row runs from the top-left row by row; random shuffles 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" and object-cover, and the cell's aspect-ratio reserves 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 / li list; with onSelect each cell is a type="button", reachable with Tab, activated with Enter / Space, and shows a focus-visible ring
  • With images and clickable cells, the button uses the image alt as its aria-label; non-clickable images keep their alt
  • The centre overlay ignores pointers; the counter region is aria-live="off", screen readers get only the full value, and rolling digits are hidden with aria-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