CMS Filter Bar with Chips & Tabs
CMS Filter Bar with Chips & Tabs is the simplest way to add interactive filtering to any CMS-bound grid in Framer. Map a Categories collection to a chip row, drop a tab bar above your grid, and watch non-matching cards disappear in real time — no per-category code edits, no smart components, no plugins.
A drop-in interactive filter system for any CMS-bound grid in Framer — built entirely on the canvas. Three tiny code overrides do the wiring; everything visual stays as native Framer frames, components, and CMS collections.
Chip filters — click a chip → grid narrows to that category.
Tab filters — click a tab → grid narrows to that media type.
Combinable — chip + tab filters stack with AND logic (e.g. Nature + Videos).
Case-insensitive — Sports, sports, SPORTS all behave identically.
Plural / singular tolerant — a "Videos" tab matches CMS items typed as "Video".
Hide / show — non-matching cards collapse via display: none so the grid reflows cleanly.
CMS-Driven Chips: Bind the filter chip row to a Categories collection — adding a new chip is just adding a new CMS item, never a code change.
Combinable Filters: Chip + tab filters stack with AND logic for clean multi-dimensional narrowing.
Case-Insensitive & Plural-Tolerant: Robust to any label casing or plural/singular mismatch between your chip text and CMS values.
Built-In States: Default, hover, and active styles with proper colour contrast — including the white-on-dark cascade that beats Framer's inline text colour.
Fully Responsive: Works across desktop, tablet, and mobile breakpoints — the overrides are layout-agnostic and inherit your canvas's responsive sizing.
Self-Configuring: Each override reads the chip or tab's own label text at runtime — duplicate, relabel, done.
Zero Code Edits: Three named overrides cover everything. Extend by editing the CMS, never the source.
Discover Pages: Image and video galleries with category chips and a media-type tab bar (All Media / Videos / Images).
Course & Class Catalogs: Strength / Cardio / Yoga chips above a class card grid, with All / Live / On-Demand tabs.
App Stores & Directories: Finance / Business / Health filter pills above an app grid.
Marketplaces & Resource Hubs: Filter products, articles, or templates by tag without touching the canvas every time you add a new tag.
Three named overrides, all exported from a single Filtering.tsx file:
Shared state lives in a single createStore so every component re-renders together on click.
Add a CMS collection with the fields you want to filter on — typically a Title and an enum or string Category (and optionally a Type enum like Image / Video).
Drop the file into your Framer project under Assets → Code → New Code File and paste Filtering.tsx.
On your canvas, build a chip row. A plain frame with a text child is enough. Label each chip with the value it filters to (All, Nature, Sports, …).
Apply the overrides:
Each chip → File Filtering → Override withFilterChip
Each tab → File Filtering → Override withFilterTab
The CMS repeater's per-item wrapper → File Filtering → Override withFilteredCard
Publish. Click around — it works.
This is the killer feature. The filter chip row can be CMS-driven too:
In your CMS, add a new category (e.g. "Sports") to either:
A standalone Categories collection whose items drive a chip repeater, or
Directly to the Category enum on your main collection.
If you're using a chip repeater, the new category renders as a new chip automatically.
If you added chips manually, duplicate any existing chip frame and change the label text to "Sports".
Re-apply the withFilterChip override to the new chip — done.
No source edits. No new exports. No factory functions. The override reads the chip's label at runtime, so any text you type becomes a working filter.
All three overrides share a single createStore({ activeCategory: "all", activeType: "all" }). Click handlers on the chips and tabs write to that store; the card wrapper subscribes to the store and inspects the rendered text of its own children ([data-framer-name="Meta"] p for category, [data-framer-name="BadgeLabel"] p for type) to decide whether to render.
The type-match algorithm is intentionally forgiving:
JavaScript
const tA = norm(badgeText) // "video"
const tS = store.activeType // "videos"
const typeOk =
tS === "all" ||
tA === tS ||
tA.startsWith(tS) ||
tS.startsWith(tA)