WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/paginated-wave-grid.json

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
<PaginatedWaveGrid />

Installation

npx shadcn@latest add https://webberui.com/r/paginated-wave-grid.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) => ReactNodeRender 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
pagenumberControlled current page number (1-based)
defaultPagenumber1Initial page number in uncontrolled mode
onPageChange(page: number) => voidCallback fired when the page number changes
showControlsbooleantrueWhether to show the built-in pagination control bar
getItemKey(item: T, index: number) => KeyProduces a stable key for each item
aria-labelstring"分頁波次網格"Accessible label (the built-in default is Traditional Chinese — pass this prop to name it in your own language)
classNamestringApplied to the outermost container
gridClassNamestringApplied to the grid container
itemClassNamestringApplied 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.

On this page