Source Code

<div class="wrap">
  <h2>Product List</h2>
  <button class="filter-btn" id="filterBtn">
    <svg class="funnel-icon" id="funnelIcon" viewBox="0 0 64 64" width="20" height="20">
      <path id="funnelPath" d="" fill="none" stroke="currentColor" stroke-width="4.5" stroke-linejoin="round" stroke-linecap="round"></path>
    </svg>
    <span>Filters</span>
    <span class="badge" id="badge">0</span>
  </button>

  <div class="panel" id="panel">
    <label class="opt"><input type="checkbox" data-tag="in-stock"> In stock</label>
    <label class="opt"><input type="checkbox" data-tag="on-sale"> On sale</label>
    <label class="opt"><input type="checkbox" data-tag="new"> New arrivals</label>
  </div>
</div>

Filter Icon Shape Morph — Active State CSS JS

Filter Icon Active-State Morph · Animations · Plain HTML, CSS & JS · Live preview

What's included

Features

Genuine SVG path interpolation between a 10-point rest funnel and a 10-point active funnel outline
morphTo() always interpolates from the two canonical endpoint states, avoiding drift on rapid toggles
Icon shape, border color, and badge are all driven from one updateState() source of truth
Live checkbox count reflected in a bouncy scale-in badge with cubic-bezier overshoot
Stroke-based outline path keeps the funnel readable at small icon sizes
requestAnimationFrame-driven morph with ease-out quad timing
Zero dependencies — no GSAP MorphSVG plugin, no KUTE.js
Collapsible filter panel with plain checkbox inputs
Mobile (375px), Tablet (768px), Desktop device preview buttons
Live split-pane editor — preview updates as you type

About this UI Snippet

Filter Icon Shape Morph — Funnel Path Interpolation Between Idle and Active States

Screenshot of the Filter Icon Active-State Morph snippet rendered live

Most filter buttons only swap a background color when active. This snippet goes further: the funnel glyph itself morphs shape — narrowing and tightening — the instant a filter is applied, using genuine SVG path point interpolation rather than a static icon swap. A count badge scales in alongside the morph, giving a single control three layers of feedback: shape, color, and count.

Two point-matched funnel outlines

restPts and activePts are each a 10-point closed outline in a shared 0–64 SVG viewBox, describing the same funnel silhouette at two different states of "openness". Because both arrays have exactly 10 points in the same drawing order, each vertex in restPts has a direct partner in activePts to travel toward. pointsToPath() turns either array into an SVG path d string of M/L commands (the path is stroked, not filled, so it stays an outline rather than a solid funnel), and lerpPath(a, b, t) linearly blends every coordinate pair between the two arrays at progress t.

Driving the narrowing animation

morphTo(target, duration) runs a small requestAnimationFrame loop that always interpolates between the two canonical states — restPts and activePts — rather than from whatever the path currently happens to be. This matters: if you interpolated from the live, possibly mid-animation d attribute, rapid toggling could accumulate drift and the funnel would slowly lose its intended shape. Always sourcing from the two known-good endpoints keeps every toggle, however fast, visually consistent.

Wiring it to real filter state

updateState() counts the checked checkboxes inside .panel, decides whether any filter is active, and drives three things off that single boolean: the button gets an .active class (for the border and text color), the badge text updates to the live count, and morphTo() is called with whichever point set matches the new state. This means the icon shape is never manually toggled — it always reflects the real underlying filter state, so it cannot desync from what the user actually has selected.

The badge

The count badge uses a separate, simpler animation: transform: scale(0) at rest, transitioning to scale(1) with a bouncy cubic-bezier(0.34, 1.56, 0.64, 1) overshoot curve when .show is added. Keeping the badge's entrance on a CSS transition (rather than JS-driven interpolation like the funnel) shows that these two morphing techniques — CSS transform transitions for simple property changes, and JS point interpolation for actual path shape changes — compose well together in the same component without one blocking the other.

Designing your own state-dependent icon morph

The general recipe: pick a real, meaningful UI state (here, "any filter active"), design two versions of the same icon's silhouette with an identical, correspondingly-ordered vertex count, and drive the swap through a single state-derivation function rather than toggling classes from multiple click handlers. This keeps the icon morph, the badge, and the border color change all reachable from one source of truth, which avoids the classic bug where an icon and its underlying state get out of sync after a few interactions.

Where else this pattern helps

Any icon whose meaning depends on a data-driven boolean or count — a bell that "fills in" when there are unread notifications, a sort icon that flips orientation when direction changes, a bookmark that thickens when saved — benefits from the same rest/active point-pair-and-badge structure shown here.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet into an AI coding assistant like Claude and ask it to explain why morphTo() always interpolates from the two canonical point arrays rather than the live SVG path — that design choice is what keeps rapid filter toggling from visually drifting. It is also a good jumping-off point for extension: ask the assistant to help you wire updateState() to a real state management store (Redux, Zustand, a URL query string) instead of local checkboxes, or to design a third "many filters active" funnel state and extend the interpolation to a 3-way morph.

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 filter button in plain HTML, CSS, and JavaScript whose funnel icon morphs shape (not just color) between a relaxed "no filters" outline and a narrower "filters active" outline, with a count badge that scales in — no animation library.

Requirements:
- Represent the funnel icon in two states as arrays of [x, y] point pairs with the exact same length and order, in a shared SVG viewBox.
- Write a function that converts a point array into a stroked SVG path "d" string, and a lerp function that linearly interpolates every coordinate between two same-length arrays at a progress value t.
- Drive the shape morph with requestAnimationFrame, and make sure the interpolation always starts from one of the two canonical point arrays (never from whatever the path currently is) so rapid toggling never drifts the shape.
- Add a small filter panel with a few checkboxes. Whenever any checkbox is checked or unchecked, recompute the checked count, morph the funnel to the active or rest shape accordingly, update a badge with the live count, and scale the badge in or out with a bouncy CSS transition.
- All three visual changes (icon shape, button border color, badge) must derive from one function that reads the current checkbox state, so the icon can never fall out of sync with the real filter state.

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.

Step by step

How to Use

  1. 1
    Check a filter optionToggle any checkbox in the panel — the funnel icon narrows and the badge scales in with the live count.
  2. 2
    Uncheck all filtersWhen the count returns to zero, the funnel relaxes back to its wide rest shape and the badge scales out.
  3. 3
    Add more filter optionsAdd more <label class="opt"> checkboxes inside .panel — updateState() automatically counts every checkbox in the panel.
  4. 4
    Redesign the two funnel statesEdit restPts and activePts — keep both arrays at 10 points in the same order for a clean interpolation.
  5. 5
    Adjust the morph speedChange the 320 (ms) argument passed to morphTo() inside updateState().
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

E-commerce filter bars
Show shoppers at a glance whether any filters are active before they even read the badge count, by making the icon itself change shape.
Data table and dashboard filters
Pair with a chip filter or search results panel so the trigger icon visually confirms the filtered state of the table below it.
Toolbar icon states
Extend the rest/active point-pair pattern to any toolbar icon whose meaning changes with application state, not just filters.
Learn state-driven SVG morphing
A compact example of deriving an icon shape morph, a badge animation, and a color change from one shared boolean/count instead of separate toggles.
Accessible filter indicators
Combine the visual morph with an aria-live region announcing the active filter count for screen reader users.
Admin panel search tools
Use the same funnel morph on an admin table search/filter toolbar to make the active-filter state immediately scannable across a busy interface.

Got questions?

Frequently Asked Questions

A color change alone is easy to miss at a glance, especially for colorblind users or on a busy toolbar. Narrowing the funnel shape itself adds a second, shape-based signal that active filtering is happening, independent of color.

If the interpolation source were the live d attribute, rapid repeated toggles mid-animation could compound small floating point drift and gradually distort the funnel. Always starting from one of the two known-good canonical arrays keeps every toggle visually identical no matter how fast the user clicks.

Yes — remove fill="none" and the stroke attributes, add a fill color, and design both point arrays as the outline of a solid shape rather than a single-width stroke path.

Store a third point array with the same 10-point count and call morphTo() with whichever pair of arrays represents the current transition — every array needs matching point counts for any-to-any interpolation to stay clean.

No — the badge uses a simple CSS transform: scale() transition with a bouncy cubic-bezier curve. Only the funnel outline itself needs JS-driven path interpolation because its shape, not just a transform property, is changing.

Replace the checkbox count logic inside updateState() with whatever boolean/count your app already tracks (URL query params, a filter store, etc.), and call updateState() whenever that state changes.