Skip to content
WANCRAFT

B.100Cards & Containers

Filterable Grid

A grid filtered by tag chips and sorted from a menu, with items gliding between layouts as filters change.

NewReactMotionTailwindintermediate

Overview

Filterable Grid is the portfolio and resource-library pattern done carefully. Tag chips filter the items — one at a time or several at once, matching any or all of the picked tags — and a sort menu reorders them. When the set changes, items that stay glide to their new cells with a layout spring, leaving items pop out of the flow and shrink away without holding up the rest, and newcomers grow in. A live “Showing 6 of 12” count is announced politely, an empty state offers to clear the filters, and a render prop lets you draw each item however you like while the grid keeps the animation, keys and filtering logic.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/filterable-grid.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/filterable-grid.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/filterable-grid.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { FilterableGrid, type FilterableItem } from "@/components/ui/filterable-grid";

export function FilterableGridExample() {
  const projects: FilterableItem[] = [
    { id: "atlas", title: "Atlas", tags: ["Web", "Brand"], content: "Rebrand and marketing site." },
    { id: "tide", title: "Tide", tags: ["Mobile"], content: "Surf forecast app." },
    { id: "kiln", title: "Kiln", tags: ["Brand", "Motion"], content: "Identity in motion." },
    { id: "orbit", title: "Orbit", tags: ["Web", "3D"], content: "Interactive product tour." },
  ];

  return (
    <FilterableGrid items={projects} />
  );
}

03Props

PropTypeDefaultDescription
itemsT extends FilterableItem[]—{ id: string; title: string; tags: string[]; content?: ReactNode } plus any fields your renderItem needs.
renderItem(item: T) => ReactNode—Render prop for each item. Defaults to a card with title, content and tags.
filtersstring[]—Chips in order. Defaults to every tag in order of first appearance.
selection"multi" | "single""multi"Several tags at once, or one at a time.
match"any" | "all""any"With several tags: items with any of them, or with all of them.
selected / defaultSelectedstring[]— / []Controlled or initial selected tags. Empty shows everything.
onSelectedChange(selected: string[]) => void—Called when the chip selection changes.
sortOptions{ value: string; label: string; compare?: (a: T, b: T) => number }[]Featured, A–Z, Z–ASort menu choices; the first is the initial sort.
showSortbooleantrueShow the sort menu.
showCountbooleantrueShow “Showing x of y” (it stays announced when hidden).
countLabel(shown: number, total: number) => string—Text of the count and its announcement.
showTagCountsbooleantrueShow how many items carry each tag.
emptyStateReactNode"Nothing matches these filters."Shown when nothing matches, above a Clear filters button.
minItemWidthnumber220Smallest column width in px (auto-fill grid).
gapnumber16Gap between items in px.
spring{ stiffness: number; damping: number; mass: number }{ stiffness: 400, damping: 36, mass: 0.9 }Spring for items moving to new cells.
labelstring"Filter by tag"Accessible name of the chip group.
accentstring"#FF7448"Active chip and focus colour.
...propsHTMLAttributes<HTMLDivElement>—Forwarded to the root element.

04Features

  • 01Tag chips with single or multi select, matching any or all tags
  • 02Sort menu with default or custom comparators
  • 03Layout animation: staying items glide, leaving items pop out (popLayout), new items grow in
  • 04Live “Showing x of y” count and an empty state with Clear filters
  • 05Render prop for items; generic over your item type
  • 06Responsive auto-fill grid with a minimum column width
motion · layoutmotion · springinput · clickinput · keyboardinput · focus

05Accessibility

  • Chips are toggle buttons with aria-pressed inside a labelled role="group"; “All” is pressed when nothing is filtered.
  • Tag counts are read as “Web, 5 items” rather than a bare number.
  • The count is a polite live region, so filtering is announced (“Showing 6 of 12”); it stays in the DOM, visually hidden, when showCount is off.
  • Sorting uses a native, labelled select element.
  • With prefers-reduced-motion items appear and disappear instantly with no layout animation.

06Limitations

  • Layout animation measures every visible item; for hundreds of items, paginate or virtualise first.
  • Focus on a chip is kept, but focus inside an item that is filtered out is lost with it.