Filter-Reactive Carousel — HTML CSS JS Snippet

Filter-Reactive Carousel · Carousels · Plain HTML, CSS & JS · Live preview

What's included

Features

Track rebuilt from scratch on every filter change, so the entrance animation replays every time, not just once
Items, categories, and rendering logic kept fully separate — adding a product is a one-line data change
Staggered scale-and-fade entrance driven by each card's position within the filtered results, not the full list
A real, labeled empty state instead of a silently blank track when a filter matches nothing
Filter chips are plain data-driven buttons — highlighting the active one needs no separate state variable
Horizontally scrollable track works at any item count without a layout redesign

About this UI Snippet

Filter-Reactive Carousel — Rebuilding the Track on Every Filter Change

Screenshot of the Filter-Reactive Carousel snippet rendered live

A filterable grid usually just hides non-matching items in place — this carousel instead *rebuilds the track from scratch* on every filter change, which is what makes the staggered entrance animation possible: every surviving card is a genuinely new DOM element, so its animation: frcIn .35s ... forwards runs fresh every single time, not just the first time it appears.

Data, chips, and cards kept separate on purpose

The items live in one plain ITEMS array — each with a cat field — completely separate from the chip buttons and the rendering logic. render() doesn't know or care how many categories exist; it just filters ITEMS by whatever activeCat currently is and rebuilds the track. That separation is what makes adding an eighth product or a sixth category a one-line data change, never a rendering-logic change.

The stagger is just an index multiplied by a delay

Each surviving card gets animationDelay = (i * 45) + 'ms' where i is its position in the *filtered* result, not its original position in ITEMS. That's the detail that makes the stagger always look clean regardless of which filter is active — card 1 of a 3-item filtered result always starts before card 2, with the exact same 45ms rhythm every time, even though card 2 might have been item 6 in the full unfiltered list.

A real empty state, not a blank track

When a filter matches nothing, render() doesn't just leave the track empty — it explicitly inserts a labeled empty-state message, because a silently blank carousel reads as broken, not as "no results."

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why rebuilding the entire track from scratch on every filter change (rather than toggling a hidden class on existing cards) is necessary for the staggered entrance animation to replay every single time. It's also worth asking the assistant to convert the single-select category chips into multi-select filters using a Set, or to add a result-count aria-live announcement so screen reader users know how many items matched after each filter change.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a filterable, animated carousel in plain HTML, CSS, and vanilla JavaScript where clicking a category chip re-filters a horizontally scrollable row of cards with a staggered entrance animation — no library.

Requirements:
- A plain JavaScript array of item objects, each with at minimum an icon/label and a category field, kept completely separate from any DOM markup — no items hardcoded directly in the HTML.
- A row of category filter chip buttons (including an "All" option) above the card track.
- A single render function that filters the items array by whichever category is currently active, completely clears and rebuilds the card track's DOM contents from that filtered result (not merely hiding/showing existing elements), and appends one newly-created card element per matching item.
- Each newly-created card must play a CSS keyframe entrance animation (a combination of scale and opacity, ending at full size and full opacity) every single time it is created, including when re-filtering back to a category that was shown before — this requires that re-filtering literally recreates the DOM elements rather than reusing them.
- Cards must animate in with a staggered delay based on their position within the CURRENT filtered results (not their original position in the full unfiltered array), so the stagger timing always looks correct regardless of which filter produced the current set.
- Clicking a chip must visually mark it as the active filter (and unmark the previously active one) and trigger a re-render.
- If a filter produces zero matching items, display a clear, explicitly-labeled empty state message in the track rather than leaving it silently blank.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="frc-wrap">
  <div class="frc-chips" id="frcChips">
    <button class="frc-chip frc-chip-active" data-cat="all">All</button>
    <button class="frc-chip" data-cat="audio">Audio</button>
    <button class="frc-chip" data-cat="wear">Wearables</button>
    <button class="frc-chip" data-cat="play">Gaming</button>
    <button class="frc-chip" data-cat="home">Home</button>
  </div>
  <div class="frc-track" id="frcTrack"></div>
</div>

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAll eight cards appear with a staggered scale-in entrance under the "All" filter.
  2. 2
    Click a category chipThe track rebuilds — matching cards animate in fresh, non-matching ones disappear instantly.
  3. 3
    Click back to "All"Every card re-enters with the same staggered animation, since it's a brand-new render.
  4. 4
    Add a ninth itemAdd one object to the ITEMS array with an icon, label, category, and gradient — it appears automatically under the right filter.
  5. 5
    Add a sixth categoryAdd one more chip button with a matching data-cat value; no JS changes needed.

Real-world uses

Common Use Cases

Product category browsers
Filter a featured-products carousel by category with a satisfying re-entrance animation each time.
Portfolio filtering by skill or type
Let visitors filter project cards by discipline — design, code, writing — with fresh animated results.
Content hubs filtered by topic
Show blog posts, videos, or resources filtered by tag in a scrollable, animated strip.
Event or session schedule filters
Filter a conference schedule carousel by track or time slot with clear visual feedback per change.

Got questions?

Frequently Asked Questions

Add a new .frc-chip button with a matching data-cat value in the HTML, and give at least one ITEMS entry that same cat value — the filtering logic works generically off whatever categories actually appear in the data.

Hiding cards in place would mean a card that was already visible under the previous filter never re-triggers its CSS animation, since the element never left the DOM. Rebuilding from scratch guarantees every visible card is a fresh element, so the entrance animation always plays.

The current logic supports one active category at a time (single-select chips). For multi-select, track activeCat as a Set instead of a string, toggle membership on chip click, and change the filter test to check whether the item's category is in that set.

Change the 45 in animationDelay = (i * 45) + 'ms' — a larger number spreads the entrance out more, a smaller one makes it feel closer to simultaneous.

Chips are real <button> elements, and the active one is visually distinguished — for full accessibility, add aria-pressed reflecting each chip's active state and consider an aria-live region announcing the result count after each filter change.