Grid Systems

Paginated Wave Grid

A paginated grid layout — on a page change the outgoing items exit in distance-based waves toward the turn direction while the new page's items flood in from the opposite side, with the page number and total rolling and morphing in sync.

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 paginated-wave-grid.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.

3
6
40
0.05
0.5
Active edited usageDefault props
<PaginatedWaveGrid/>

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/paginated-wave-grid.en.json

Or, once registries are configured in components.json, install it as @webberui/paginated-wave-grid.

Usage

import { PaginatedWaveGrid } from "@/components/ui/paginated-wave-grid";

const items = Array.from({ length: 18 }, (_, i) => ({ id: i, label: `#${i}` }));

<PaginatedWaveGrid
  items={items}
  pageSize={6}
  columns={3}
  getItemKey={(item) => item.id}
  renderItem={(item) => (
    <div className="flex h-24 items-center justify-center rounded-xl border">
      {item.label}
    </div>
  )}
/>;

The turn direction is derived automatically from the previous and next page numbers: paging forward, the old page's items exit leftward in waves ordered by "distance from the left edge" while the new page's items flood in from the right; paging backward reverses the whole set. At the bottom, the page number and total roll digit by digit to their new values like an odometer.

Controlled mode

Passing page and onPageChange switches to controlled mode, letting it stay in sync with external state or other controls:

const [page, setPage] = React.useState(1);

<PaginatedWaveGrid page={page} onPageChange={setPage} items={items} renderItem={/* … */} />;

Props

PropTypeDefaultDescription
itemsT[]—Data array, sliced into pages by pageSize
renderItem(item: T, index: number) => ReactNode—Render function for a single item
pageSizenumber6Items per page
columnsnumber3Number of grid columns
gapnumber12Grid gap (px)
travelnumber40Horizontal offset of the wave as items enter and exit (px)
waveStaggernumber0.05Delay between each distance step (seconds), setting the speed of the ripple
rowWeightnumber0.5Weight of the row direction in the wave step count; >0 produces a diagonal ripple
durationnumber0.5Duration of a single item entering or exiting (seconds)
blurbooleantrueWhether entering and exiting are accompanied by a blur
loopbooleanfalseWrap around from the last page to the first and back
pagenumber—Controlled current page number (1-based)
defaultPagenumber1Initial page number in uncontrolled mode
onPageChange(page: number) => void—Callback fired when the page number changes
showControlsbooleantrueWhether to show the built-in pagination control bar
getItemKey(item: T, index: number) => Key—Produces a stable key for each item
aria-labelstring"Pagination wave grid"Accessible label (the English source includes this default — pass this prop to name it in your own language)
classNamestring—Applied to the outermost container
gridClassNamestring—Applied to the grid container
itemClassNamestring—Applied to the wrapper of each item

How it works

  • The exit layer is absolutely positioned over the new page (pointer-events-none), so the new page can refill the slots immediately while the old items fly away and fade out above it.
  • Each slot's wave step = leading-edge column distance + row distance × rowWeight, and exit and flood-in share the same step count, producing one continuous sweep.
  • The page number uses an odometer-style vertical film strip where each digit rolls independently to its target; when the digit count changes, keys are taken from the right, keeping the columns aligned.
  • The transition timing is finished off with a setTimeout independent of rAF, cleared on unmount and on the next page change.

Accessibility

  • The built-in previous page / next page buttons carry aria-label and keyboard focus styles, and are disabled on the first and last page (not disabled when loop is on).
  • When the container is focused, the ← / → arrow keys turn pages.
  • A hidden region with aria-live="polite" announces "page X of Y" on every page change.
  • The odometer digits are hidden from assistive technology (aria-hidden) so that digit-by-digit reading does not become noise.
  • When the user has "reduce motion" enabled at the system level, the wave offset, blur, and rolling are disabled and a page change switches the layout directly.