ui-022

Filter Chips

£10 or the whole Vault, £99

Filter chips for product and portfolio grids: multi-select chips above results that re-flow with GSAP Flip while the count animates.

Flip intermediate
7 more details
flip-filterfilterclick-togglecount-upstaggerdragkeyboard-navigation

This element reads better at your own screen size than in the frame below:

Updated

About this component

The filter row most catalogues, media libraries and portfolios need, with the re-flow done properly. Chips are multi-select and horizontally scrollable, so a long tag list stays on one line, under a scroll rail drawn by the component rather than the browser. Toggling one opens a tick inside the chip, which pushes its neighbours along instead of popping over them, and the grid below re-flows in a single GSAP Flip pass: cards that no longer match drop away, the survivors slide to their new places rather than jumping, and cards coming back fade up in position.

Read the full effect overview

The results count tweens to its new value instead of snapping, so removing eight cards reads as a change of scale. An optional "All" chip holds the resting state and clears the row, and Clear all collapses the active chips one after another before the grid refills. Every chip is a real button carrying aria-pressed, so the whole thing is operable from the keyboard, and left and right arrows walk the row.

One HTML file, one stylesheet, one script, vanilla JavaScript, GSAP and the Flip plugin, no framework. Ships in a light and a dark theme, switched with one data-variant attribute.

Filter Chips - UI component preview

What's included

10 items
  • Multi-select chips in a horizontally scrollable row with fades at both ends
  • A drawn scroll rail that sizes itself to the row and drags it
  • A tick that opens inside the chip and pushes its neighbours along
  • One Flip pass for all three populations: cards leaving, sliding and returning
  • Results count that counts to its new number
  • An optional All chip that holds the resting state and clears the row
  • Clear all that collapses the active chips in sequence
  • Empty state when the picked tags have nothing in common
  • Real buttons with aria-pressed; arrow keys walk the row
  • Light and dark themes on one token set

Perfect for

4 use cases
  • Media libraries and asset managers filtered by file type
  • Product listings filtered by category, material or colour
  • Portfolio and case-study grids filtered by discipline
  • Docs and blog indexes filtered by topic

How it works

5 sections

One Flip pass, three populations

A toggle records Flip.getState over every card, sets the hidden attribute on the ones that no longer match, then hands the state to Flip.from. Leaving cards are taken out of the grid with absolute:true so the survivors can travel over them, onEnter fades and scales the returning cards up, and onLeave fades the departing ones out. Flip's own scale option is never used, because it would squash the card titles for the whole tween.

Holding the layout still

absolute:true takes every card out of flow, not just the leavers, so the grid would collapse to nothing for the length of the tween and drop the rest of the page up behind the travelling cards. The grid's height is measured before and after the filter runs and tweened between the two, released only once Flip has put the cards back. An absolutely positioned grid item also stretches to fill its grid area, so the cards are set to align-self: start for the duration or each one balloons to the full height of the grid and animates back down.

The chip tick

Each chip carries a tick whose box starts at zero width with overflow hidden. Turning the chip on tweens its width, margin and opacity open, so the chip grows and the row shifts along with it. Only width, margin and opacity move: a CSS transform on the tick would later be read back by GSAP as a pixel offset. Clear all runs the same tween in reverse with a stagger, then re-flows the grid once the row has given its width back.

The scroll rail

The row's native scrollbar is hidden and drawn as a rail of its own, because Chrome drops every ::-webkit-scrollbar rule as soon as scrollbar-width is set, which leaves a browser-default light bar under the row. The grip is sized from the visible fraction of the row, positioned from scrollLeft with a quickSetter rather than a tween so it cannot trail the wheel, and drags the row back when pulled. It fades out entirely when the chips fit, and a fade lights at each end only when there is more row that way.

The counting total

The count is tweened through a proxy object rather than written straight out, with the rounded value pushed into the element on every update and the exact number set on completion, so it always lands on the truth. One proxy serves the whole component: a fresh object per toggle left the previous tween alive and writing its own numbers into the same element. Under prefers-reduced-motion the same call writes the final number immediately.

Plugins Flip
Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this UI element's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid UI element

Purchase to unlock the code.

Buying Filter Chips opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£10 Standard license, unlimited projects

Browse UI elements

Or the whole library: the Vault, £99, one payment.

Documentation

What's Included

  • index.html: the demo page and the markup to copy

  • assets/style.css: component styles, plain CSS custom properties for colours

  • assets/script.js: readable, commented source with an onReady guard

  • Multi-select chips in a horizontally scrollable row with fades at both ends

  • A drawn scroll rail that sizes itself to the row and drags it

  • A tick that opens inside the chip and pushes its neighbours along

  • One Flip pass for cards leaving, sliding and returning

  • Results count that counts to its new number

  • An optional "All" chip that holds the resting state and clears the row

  • Clear all that collapses the active chips in sequence

  • Empty state when the picked tags have nothing in common

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

2. Add to your <body>:

Copy the whole .fcb block from index.html. A chip names the tag it filters on:

Code snippet omitted: it ships with the download.

A chip carrying data-clear instead of data-tag is the optional "All" chip: it holds no tag of its own, reads as pressed while nothing is filtered, and clears the row when picked. Leave it out and nothing else changes.

A card lists the tags it carries, space separated:

Code snippet omitted: it ships with the download.

The noun beside the count comes off the count element, so the component can count anything:

Code snippet omitted: it ships with the download.

Chips and cards are read on load, so add or remove as many as you like. Selecting more than one chip narrows to the cards that carry all of them; change one line in reflow() for a union instead (see Customisation).

3. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

How It Works

One Flip pass, three populations. A toggle records Flip.getState over every card, sets the hidden attribute on the ones that no longer match, then hands the state to Flip.from. Leaving cards are taken out of the grid with absolute: true so the survivors can travel over them, onEnter fades and scales the returning cards up, and onLeave fades the departing ones out. Flip's own scale option is never used, because it would squash the card titles for the whole tween.

Holding the layout still. absolute: true takes every card out of flow, not just the leavers, so the grid would collapse to nothing for the length of the tween and drop the rest of the page up behind the travelling cards. The grid's height is measured before and after the filter runs and tweened between the two, released only once Flip has put the cards back. An absolutely positioned grid item also stretches to fill its grid area, so the cards get align-self: start for the duration of the pass, or each one balloons to the full height of the grid and animates back down.

The scroll rail. The row's native scrollbar is hidden and drawn as a rail of its own, because Chrome drops every ::-webkit-scrollbar rule as soon as scrollbar-width is set, which leaves a browser-default light bar sitting under the row. The grip is sized from the visible fraction of the row, positioned from scrollLeft with a quickSetter rather than a tween so it cannot trail the wheel, and drags the row back when pulled. It fades out entirely when the chips fit.

The chip tick. Each chip carries a tick whose box starts at zero width with overflow: hidden. Turning the chip on tweens its width, margin and opacity open, so the chip grows and the row shifts along with it. Only width, margin and opacity move: a CSS transform on the tick would later be read back by GSAP as a pixel offset. Clear all runs the same tween in reverse with a stagger, then re-flows the grid once the row has given its width back.

The counting total. The count is tweened through a proxy object rather than written straight out, with the rounded value pushed into the element on every update and the exact number set on completion, so it always lands on the truth. One proxy serves the whole component: a fresh object per toggle left the previous tween alive and writing its own numbers into the same element.

Using It With Your Own Design

The component needs three things from your markup: a row of chips carrying data-tag, a grid of cards carrying data-tags, and an element to write the count into. The card's own layout is entirely yours; the demo's preview block, name, type tag and size line are just filler.

The demo page's furniture is not part of what you bought. The 44px toolbar strip at the top (theme toggle, reset button) lives only in index.html, and the .stage / .stage__inner wrapper is there to centre the component on the demo's grey ground. Neither is referenced by assets/script.js; drop both and paste the .fcb block straight into your own page.

The non-obvious CSS the script depends on:

  • .fcb-card[hidden] { display: none } is restated because the card rule sets display: flex. Without it the filter looks like it does nothing.
  • .fcb-check starts at width: 0 with overflow: hidden, and carries no CSS transform, because GSAP animates its box.
  • The chip row is a plain overflow-x: auto scroller with no data-lenis-prevent. On a horizontal-only scroller that attribute takes a host page's smooth scroll off the vertical wheel too, and the page jump-scrolls whenever the cursor is over the row.
  • The end fades are ::before and ::after on the wrapper with pointer-events: none, so they never eat a click on the first or last chip. Each is lit by an at-start / at-end class the script sets, so a row that fits shows no false edge.
  • .fcb-bar sets scrollbar-width: none and hides ::-webkit-scrollbar; the rail markup (.fcb-rail and .fcb-rail-grip) replaces it. Drop both if you would rather keep the native bar.
  • .fcb-grid is position: relative so Flip's absolutely positioned cards anchor to the grid rather than to the page.

Themes

The component ships in two themes, light and dark: one design at two token values. Pick one with the attribute, because the ground colour is part of it:

Code snippet omitted: it ships with the download.

Nothing else changes: same markup, same script, and nothing in assets/script.js reads the theme name. The demo's toolbar toggle and the ?variant=light URL parameter only set that attribute.

Every colour, radius and shadow is a custom property in the two body[data-variant="..."] blocks at the top of assets/style.css:

Group Properties
Ground and ink --ground, --ground-2, --raised, --hover, --ink, --ink-2, --ink-3
Hairlines --line, --line-strong
Accent --accent (the selected chip and the focus ring), --accent-ink (text on it)
Shape and type --font, --radius, --radius-sm, --radius-xs, --shadow
The card's surfaces --thumb-bg, --thumb-fg, --tag-bg, --tag-fg

To wear your own brand, re-value --accent and --accent-ink (which must clear 4.5:1 against it); to sit on your own surfaces, re-value --ground, --raised and --line. Give text a solid colour rather than a translucent one, or its contrast cannot be measured.

Customisation

What Where
Re-flow speed and feel const move = dur(0.5) and power2.inOut in reflow()
Card enter and leave the onEnter and onLeave tweens in reflow()
Match rule (all tags vs any) swap .every( for .some( in reflow()
Tick open width: 14 and back.out(2) in setChipTick
Clear-all cadence the i * 0.05 delay in clearAll
Count speed duration: dur(0.5) in countTo
Count noun data-singular / data-plural on the noun element
Rail thickness and colour .fcb-rail height and the --ground-2 / --line-strong tokens
Minimum grip width Math.max(28, ...) in railSync
Grid columns grid-template-columns on .fcb-grid
Colours, radii, shadows the body[data-variant] blocks in assets/style.css

Accessibility

  • Chips are real <button> elements carrying aria-pressed, so a screen reader announces each one as a toggle and Enter or Space works with no extra code.
  • The row is a role="group" with a label, and left and right arrows move focus along it, including the "All" chip.
  • :focus-visible rings are kept on every chip, the clear button and the demo's toolbar controls.
  • Hiding a card uses the hidden attribute, so filtered-out cards leave the accessibility tree as well as the layout.
  • Without JavaScript every card is visible and readable; only the filtering is lost.
  • prefers-reduced-motion: reduce sets every duration to zero, so the grid re-flows and the count updates instantly rather than not at all.

Requirements

  • GSAP 3.12+ and the Flip plugin
  • No build step, no framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.