WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/shove-reflow-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.

128
<ShoveReflowGrid />

Installation

npx shadcn@latest add https://webberui.com/r/shove-reflow-grid.json

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

PropTypeDefaultDescription
itemsShoveReflowItem[]Grid items, each carrying id, categories?, and content
filtersShoveReflowFilter[]derived from itemsFilter options; when not supplied they are derived, deduplicated, with "all" prepended
valuestringControlled: the currently selected category value
defaultValuestringALL_CATEGORYUncontrolled: the initially selected category value
onValueChange(value: string) => voidCallback fired when the selected category changes
intensity"subtle" | "normal" | "vigorous""normal"Shove strength, affecting the springiness of the return and the fall distance
minItemWidthnumber128Minimum card width (px); the grid wraps by auto-fill against it
showFilterBarbooleantrueWhether to show the built-in filter bar
allLabelReact.ReactNode"全部"Wording of the "all" option (the built-in default is Traditional Chinese — pass this prop to word it in your own language)
itemClassNamestringclassName 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 layout FLIP paired with AnimatePresence's popLayout mode. 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 popLayout and then plummets with an accelerating ease, tumbling, shrinking, and fading out; the direction is derived from the id rather than being uniform, so it looks like being squeezed out of a crowd.
  • Strength tiers: intensity adjusts 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. vigorous is the bounciest and most exaggerated, subtle the 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 through aria-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.

On this page