Shove Reflow Grid
A shove reflow grid — switching category makes incoming cards squeeze into the lineup, shoving their neighbors aside before they spring back, while outgoing cards get pushed past the edge and tumble away.
npx shadcn@latest add https://webberui.com/r/shove-reflow-grid.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ShoveReflowGrid />
Installation
npx shadcn@latest add https://webberui.com/r/shove-reflow-grid.jsonOr, once registries are configured in components.json, install it as @webberui/shove-reflow-grid.
Usage
import {
ShoveReflowGrid,
type ShoveReflowItem,
} from "@/components/ui/shove-reflow-grid";
const items: ShoveReflowItem[] = [
{ id: "apple", categories: ["Fruit"], content: <Card>🍎 Apple</Card> },
{ id: "carrot", categories: ["Vegetables"], content: <Card>🥕 Carrot</Card> },
{ id: "cake", categories: ["Desserts"], content: <Card>🍰 Cake</Card> },
];
<ShoveReflowGrid items={items} intensity="normal" />;When filters is not supplied, the filter bar is derived from the items' categories, deduplicated, with an "all" option prepended.
Controlled mode
To use your own category-switching UI (tabs or a sidebar, for example), turn off showFilterBar and drive it from outside with value and onValueChange; the ALL_CATEGORY constant stands for "show all":
import { ShoveReflowGrid, ALL_CATEGORY } from "@/components/ui/shove-reflow-grid";
const [category, setCategory] = React.useState(ALL_CATEGORY);
<ShoveReflowGrid
items={items}
showFilterBar={false}
value={category}
onValueChange={setCategory}
/>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | ShoveReflowItem[] | — | Grid items, each carrying id, categories?, and content |
filters | ShoveReflowFilter[] | derived from items | Filter options; when not supplied they are derived, deduplicated, with "all" prepended |
value | string | — | Controlled: the currently selected category value |
defaultValue | string | ALL_CATEGORY | Uncontrolled: the initially selected category value |
onValueChange | (value: string) => void | — | Callback fired when the selected category changes |
intensity | "subtle" | "normal" | "vigorous" | "normal" | Shove strength, affecting the springiness of the return and the fall distance |
minItemWidth | number | 128 | Minimum card width (px); the grid wraps by auto-fill against it |
showFilterBar | boolean | true | Whether to show the built-in filter bar |
allLabel | React.ReactNode | "全部" | Wording of the "all" option (the built-in default is Traditional Chinese — pass this prop to word it in your own language) |
itemClassName | string | — | className of the li wrapping each card |
The id on a ShoveReflowItem must be stable and unique — it is what the reflow (FLIP) animation uses to pair up before-and-after positions.
How it works
- Shoving aside: Motion's
layoutFLIP paired withAnimatePresence'spopLayoutmode. An incoming card "squeezes" into the grid at a smaller scale while the other cards spring aside to make room; the damping is kept low so they slightly overshoot on the way back, producing the feel of "shoved aside, then springing back". - Shoved out and falling: an outgoing card is frozen in place by
popLayoutand then plummets with an accelerating ease, tumbling, shrinking, and fading out; the direction is derived from theidrather than being uniform, so it looks like being squeezed out of a crowd. - Strength tiers:
intensityadjusts the return spring's stiffness and damping, the incoming card's starting scale, and the outgoing card's fall distance and tumble angle all at once.vigorousis the bounciest and most exaggerated,subtlethe most restrained. - Responsive grid:
grid-template-columns: repeat(auto-fill, minmax(minItemWidth, 1fr))wraps automatically, with the column count changing along with the container width.
Accessibility
- The filter bar is
role="toolbar", and each option is a native<button>expressing its selected state througharia-pressed. - The filter bar uses a roving tabindex: ← → (or ↑ ↓) move between options and apply immediately, and Home / End jump to the first and last.
- A built-in
aria-live="polite"status region announces how many items are currently shown whenever the category changes. - When the user has "reduce motion" enabled at the system level, the shove, the fall, and the reflow animations are disabled and it switches with a very brief fade in / fade out, with no change to the DOM structure.
Sieve Filter Grid
A sieve filter grid — filtered-out items shrink and fly into the filter chip that triggered them, the count badge pulses in sync, and the surviving items close ranks with a FLIP reflow.
Widget Board
An iOS home-screen widget grid — frosted glass cards in sm/md/lg size tiers, an edit mode where everything wiggles and can be removed, and a Smart Stack that pages on a timer.