Faceted Filter Sidebar — HTML CSS JS Snippet

Faceted Filter Sidebar · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Single-state render model
One state object holds every selection; a single render function rebuilds chips, the clear-all button, and the result count — unidirectional flow in vanilla JS.
Collapsible facet groups
toggleFacet toggles an .open class; CSS animates max-height to 0 and rotates the caret, so unknown-height panels still transition smoothly.
Dual-handle price range
Two stacked range inputs with pointer-events only on the thumbs; onPrice enforces a minimum gap and paints the selected band via left/right percentages.
Custom-property colour swatches
Each swatch reads its fill from a --c variable, so a single CSS rule styles every colour and the active ring uses a layered box-shadow.
Pure-CSS partial star rating
.stars::before clips a linear-gradient to text at --n × 20% — fractional star fills with no SVG or image assets.
Removable active-filter chips
render rebuilds the chip row from state; each chip's × calls removeChip(key, value) to clear exactly one selection and re-sync its input.
Conditional clear-all
The Clear-all control fades in via a .show class only when one or more filters are active, then resets the entire state and all inputs.
Live result count
The count decays per active facet so filtering feels instant; swap the formula for your filtered array length or a server count in production.

About this UI Snippet

Faceted Filter Sidebar — Collapsible Facets, Dual Range Slider, Colour Swatches & Active-Filter Chips

Screenshot of the Faceted Filter Sidebar snippet rendered live

Faceted search is the backbone of every catalogue, marketplace, and product listing page. When a shop has thousands of items, users do not scroll — they filter. A faceted filter sidebar lets shoppers narrow results by combining independent dimensions (category, price, colour, rating) and instantly see how many products match. This snippet implements the complete pattern in plain HTML, CSS, and vanilla JavaScript: collapsible facet groups, a dual-handle price range, clickable colour swatches, a star-rating filter, removable active-filter chips, and a clear-all action.

The single most important UX principle in faceted filtering is feedback: every selection must immediately update both the visible "active filters" summary and the result count. This snippet keeps a single state object as the source of truth and re-derives the whole UI from it in one render function — the same unidirectional-data-flow idea React popularised, written in vanilla JS.

Collapsible facet groups

Each facet is a .facet block with an .open class. The toggleFacet function toggles that class on click; the CSS animates max-height from its open value to 0 and rotates the caret svg 90 degrees. Using max-height (rather than height) lets the panel collapse with a transition even though its natural height is unknown. Long facet lists stay scannable because users can fold the groups they are not using.

Dual-handle price range

The price filter stacks two <input type="range"> elements on the same track. Both have pointer-events: none so the track shows through, and only the thumbs re-enable pointer events via ::-webkit-slider-thumb. The onPrice function reads both values, enforces a minimum gap so the handles cannot cross (min > max - 10), and sets the .range-fill element's left and right as percentages to paint the selected band. This is the standard two-thumb slider technique that avoids any library.

Colour swatches and star rating

Swatches are buttons whose colour comes from a --c CSS custom property, so one rule styles every chip. Clicking toggles an .active ring and adds or removes the colour from state.Color. The rating facet uses radio inputs and a pure-CSS star bar: .stars::before renders five glyphs and a linear-gradient clipped to text fills them to --n × 20%, giving partial-star precision without images.

Active-filter chips and clear-all

After every change, render rebuilds the chip row from state. Each chip carries a removeChip(key, value) handler that clears just that selection — unchecking the matching input or resetting the price — then re-renders. The Clear-all button only appears (.show) when at least one filter is active. The mock result count decays with each active facet so the count feels responsive; in production you would swap that for the length of your filtered dataset or an API response.

Pair this sidebar with a product card grid, a chip filter bar for quick toggles, or a pagination table to page through matches.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to trace the single state object by hand to trust it. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the render function rebuilds the chip row, the clear-all visibility, and the result count from one shared state object, and why the dual price-range sliders enforce a minimum gap between their two values. The same assistant can help optimize it — ask whether the mock result-count decay formula should be replaced with an actual filtered-array length once real product data is wired in, and whether the render function's full innerHTML rebuild on every filter change would need to become more surgical for a sidebar with dozens of facets. It's also useful for extending the sidebar: ask it to persist the whole filter state into the URL's query string so filtered views are shareable, add a text search box inside a long facet list, or add a "results preview count" next to each individual checkbox showing how many items that option alone would match. Treat the code less like a finished artifact and more like a starting point for a conversation.

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 faceted filter sidebar in plain HTML, CSS, and JavaScript with collapsible facet groups, a dual-handle price slider, color swatches, and removable filter chips — no library.

Requirements:
- A single shared state object holding the current selections for at least four facet types: a multi-select category list, a multi-select color list, a single price range (min and max), and a single-select star rating, with one render function that derives every piece of dependent UI (the active-filter chip row, a conditionally visible clear-all button, and a result count) from that one state object.
- Each facet group must be individually collapsible: clicking its header toggles an open class, and the CSS must animate the facet's body between its full height and zero height using a max-height transition combined with a rotating caret icon, not an instant display toggle.
- Implement the price range as two overlapping native range inputs on the same visual track, with pointer-events disabled on the inputs themselves and re-enabled only on their thumb via the browser-specific thumb pseudo-elements, and enforce a minimum gap so the two handles can never cross or overlap each other.
- Implement color selection as a row of round swatch buttons whose fill color comes from a single CSS custom property per swatch, with a distinct ringed "active" visual state applied via a class toggle, and a checkmark visible only on the active swatch.
- Implement a star rating filter using radio inputs paired with a pure-CSS partial-star display built from a repeated star glyph clipped by a linear-gradient tied to a CSS custom property, not five separate image or SVG elements.
- Every active selection across every facet type must appear as its own removable chip above the facet list; clicking a chip's remove control must clear only that specific selection, re-sync the corresponding form control (uncheck a checkbox, deactivate a swatch, or reset the range), and re-run the render function.
- Provide a single clear-all action that resets every facet back to its default state in one call, and make sure it is only visible at all while at least one filter is currently active.

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
    Paste HTML, CSS, and JSA sidebar appears with four facet groups — Category, Price, Colour, Rating — beside a results panel showing "260 products match".
  2. 2
    Collapse a facetClick any group heading — the panel folds with a smooth max-height transition and the caret rotates. Click again to expand it.
  3. 3
    Tick category boxesCheck Sneakers and Boots — each adds a removable chip at the top of the sidebar and the result count drops.
  4. 4
    Drag the price handlesMove the two range thumbs — the purple fill band tracks between them and the dollar labels update. The handles cannot cross.
  5. 5
    Pick colours and a ratingClick colour swatches to ring them, then choose "4★ & up" — both appear as chips and the count recalculates.
  6. 6
    Remove or clear filtersClick the × on any chip to drop that filter, or hit "Clear all" (it only shows when filters are active) to reset everything.

Real-world uses

Common Use Cases

E-commerce category pages
The primary use case — narrow a shoe, apparel, or electronics catalogue by category, price, colour, and rating. Drop it beside a product card grid.
Marketplace and listing search
Property, car, and job marketplaces use faceted sidebars to combine many filter dimensions. Pair the sidebar with a data table of results.
Documentation and content libraries
Filter articles or components by tag, type, and difficulty. Combine with a search box for free-text plus facet filtering.
Admin dashboards and reports
Refine large record sets by status, owner, date, and rating before exporting. Works alongside a pagination table for paged results.
Travel and booking sites
Filter hotels or flights by price band, star rating, and amenity colour tags — the dual range slider maps directly to nightly-price filtering.
Mobile filter sheets
On narrow screens the sidebar stacks above results; move it into a bottom sheet triggered by a "Filters" button for mobile catalogues.

Got questions?

Frequently Asked Questions

In render, replace the mock count formula with a filter over your dataset: keep an array of products and run products.filter(p => state.Category.length === 0 || state.Category.includes(p.category)) and similar checks for colour, price band, and rating. Render the matching items into the results panel and set the count to the filtered length.

On each render, serialise state into query params with URLSearchParams and call history.replaceState. On load, read the params back and pre-check the matching inputs before the first render. This makes filtered views shareable and bookmarkable — essential for SEO and paid traffic landing pages.

For facets with many options (brands, sizes), add an <input type="text"> above the list and filter the visible .check rows on oninput by matching the label text. Hide non-matching rows with display:none so users can type to find an option instead of scrolling.

Add aria-label="Minimum price" and aria-label="Maximum price" to the two range inputs and aria-valuetext updated in onPrice so screen readers announce dollar amounts. Range inputs are keyboard-operable by default (arrow keys), and the minimum-gap guard prevents an invalid inverted range.

Yes. In React, move state into useState and render chips and counts from it directly — no manual render call needed. In Vue, make state reactive with ref/reactive and bind inputs with v-model. In Angular, hold state on the component and use (change)/(input) bindings. The dual-range CSS and star-bar trick port unchanged.