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.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | — | Data array, sliced into pages by pageSize |
renderItem | (item: T, index: number) => ReactNode | — | Render function for a single item |
pageSize | number | 6 | Items per page |
columns | number | 3 | Number of grid columns |
gap | number | 12 | Grid gap (px) |
travel | number | 40 | Horizontal offset of the wave as items enter and exit (px) |
waveStagger | number | 0.05 | Delay between each distance step (seconds), setting the speed of the ripple |
rowWeight | number | 0.5 | Weight of the row direction in the wave step count; >0 produces a diagonal ripple |
duration | number | 0.5 | Duration of a single item entering or exiting (seconds) |
blur | boolean | true | Whether entering and exiting are accompanied by a blur |
loop | boolean | false | Wrap around from the last page to the first and back |
page | number | — | Controlled current page number (1-based) |
defaultPage | number | 1 | Initial page number in uncontrolled mode |
onPageChange | (page: number) => void | — | Callback fired when the page number changes |
showControls | boolean | true | Whether to show the built-in pagination control bar |
getItemKey | (item: T, index: number) => Key | — | Produces a stable key for each item |
aria-label | string | "Pagination wave grid" | Accessible label (the English source includes this default — pass this prop to name it in your own language) |
className | string | — | Applied to the outermost container |
gridClassName | string | — | Applied to the grid container |
itemClassName | string | — | 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
setTimeoutindependent of rAF, cleared on unmount and on the next page change.
Accessibility
- The built-in
previous page/next pagebuttons carryaria-labeland keyboard focus styles, and are disabled on the first and last page (not disabled whenloopis 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.
Paper Layers Scene
A scene of 3–5 torn-edge paper layers with soft cut-paper shadows that parallax on scroll or cursor, for storytelling heroes and editorial layouts.
Zoomable Density Grid
A density zoom grid — switch the number of columns per row the way iOS Photos does, with every cell reflowing smoothly through a FLIP animation.