Faceted Filter Sidebar — HTML CSS JS Snippet
Faceted Filter Sidebar · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
state object holds every selection; a single render function rebuilds chips, the clear-all button, and the result count — unidirectional flow in vanilla JS.toggleFacet toggles an .open class; CSS animates max-height to 0 and rotates the caret, so unknown-height panels still transition smoothly.pointer-events only on the thumbs; onPrice enforces a minimum gap and paints the selected band via left/right percentages.--c variable, so a single CSS rule styles every colour and the active ring uses a layered box-shadow..stars::before clips a linear-gradient to text at --n × 20% — fractional star fills with no SVG or image assets.render rebuilds the chip row from state; each chip's × calls removeChip(key, value) to clear exactly one selection and re-sync its input..show class only when one or more filters are active, then resets the entire state and all inputs.About this UI Snippet
Faceted Filter Sidebar — Collapsible Facets, Dual Range Slider, Colour Swatches & Active-Filter Chips

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