Facets — <mv-facets>
Product listing filter panel: collapsible checkbox groups with counts (“Show 2 more”), color and size swatches, a two-thumb price range synced with two inputs, a deselectable minimum rating, an in-stock switch, removable active-filter chips with “Clear all”, and a mobile drawer (native <dialog>) with a “Show 42 products” button. Emits a single mv-change with the full state.
Category Blocks Type Block (ready-made section) (<mv-facets>) Status stable Also installs button , checkbox , price , radio , rating , slider , swatch , switch Keywords facets, filters, filtres, plp, listing, price-range, chips, drawer, e-commerce, block
When to use
A product listing needs filters by category, color, size, price range, rating and stock Active filters should appear as removable chips with a clear-all action On mobile, filters must open in a drawer with a button showing the matching product count
Avoid when
A data table in an admin app needs column filters and search → use Data table instead Users are expected to type a query and pick from suggestions rather than filter → use Search Autocomplete instead
Get Facets. Included in every plan: the Pro pack ships all 226 components with the CLI and the MCP server used below.
See plans
Install
node scripts/add.mjs facets --out ./src/marvelous
AI agent with the Marvelous UI MCP server: install_components({ slugs: ["facets"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/facets/facets.js, components/facets/facets.css, components/button/button.css, components/checkbox/checkbox.css, components/price/price.js, components/price/price.css, components/radio/radio.css, core/form.js, components/rating/rating.js, components/rating/rating.css, components/slider/slider.js, components/slider/slider.css, components/swatch/swatch.css, components/swatch/swatch.js, components/switch/switch.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="fc-demo" style="width:min(100%,72rem);margin-inline:auto">
<style>
#fc-demo .plp { display:grid; gap:1.5rem 2.5rem; align-items:start }
@media (width >= 64rem) { #fc-demo .plp { grid-template-columns: 16rem minmax(0,1fr) } #fc-demo mv-facets { position:sticky; top:1rem } }
#fc-demo .bar { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.75rem; margin-bottom:1.25rem }
#fc-demo .grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(min(100%,max(9rem,30%)),1fr)); gap:1.5rem 1rem }
#fc-demo .grid > [hidden], #fc-demo .empty[hidden] { display:none }
#fc-demo .empty { padding:3rem 1rem; text-align:center; color:var(--mv-fg-muted); border:1px dashed var(--mv-border-strong); border-radius:var(--mv-radius-xl) }
</style>
<div class="plp">
<mv-facets id="fc-demo-facets" result-count="8">
<form class="mv-facets">
<div class="mv-facets-header">
<h2 class="mv-facets-title">Filters</h2>
</div>
<div class="mv-facets-body">
<details class="mv-facets-group" open data-limit="5">
<summary>Category</summary>
<div class="mv-facets-group-body">
<div class="mv-facets-options">
<label class="mv-facets-option"><input type="checkbox" class="mv-checkbox" name="category" value="knitwear"><span class="mv-facets-label">Knitwear</span><span class="mv-facets-count">42</span></label>
<label class="mv-facets-option"><input type="checkbox" class="mv-checkbox" name="category" value="shoes"><span class="mv-facets-label">Shoes</span><span class="mv-facets-count">18</span></label>
<label class="mv-facets-option"><input type="checkbox" class="mv-checkbox" name="category" value="home"><span class="mv-facets-label">Home & decor</span><span class="mv-facets-count">35</span></label>
<label class="mv-facets-option"><input type="checkbox" class="mv-checkbox" name="category" value="audio"><span class="mv-facets-label">Audio</span><span class="mv-facets-count">9</span></label>
<label class="mv-facets-option"><input type="checkbox" class="mv-checkbox" name="category" value="bags"><span class="mv-facets-label">Bags & luggage</span><span class="mv-facets-count">14</span></label>
<label class="mv-facets-option"><input type="checkbox" class="mv-checkbox" name="category" value="accessories"><span class="mv-facets-label">Accessories</span><span class="mv-facets-count">27</span></label>
<label class="mv-facets-option"><input type="checkbox" class="mv-checkbox" name="category" value="gift-cards" disabled><span class="mv-facets-label">Gift cards</span><span class="mv-facets-count">0</span></label>
</div>
</div>
</details>
<details class="mv-facets-group" open>
<summary>Color</summary>
<div class="mv-facets-group-body">
<mv-swatch>
<fieldset class="mv-swatch" data-type="color" data-check>
<legend class="mv-swatch-legend">Color</legend>
<div class="mv-swatch-options">
<label class="mv-swatch-option" style="--mv-swatch-color:oklch(0.8 0.05 75)"><input type="checkbox" name="color" value="sand"><span class="mv-swatch-label">Sand</span></label>
<label class="mv-swatch-option" style="--mv-swatch-color:oklch(0.34 0.05 262)" data-tone="dark"><input type="checkbox" name="color" value="navy"><span class="mv-swatch-label">Midnight blue</span></label>
<label class="mv-swatch-option" style="--mv-swatch-color:oklch(0.7 0.06 150)"><input type="checkbox" name="color" value="sage"><span class="mv-swatch-label">Sage</span></label>
<label class="mv-swatch-option" style="--mv-swatch-color:oklch(0.58 0.13 38)" data-tone="dark"><input type="checkbox" name="color" value="brick"><span class="mv-swatch-label">Brick</span></label>
<label class="mv-swatch-option" style="--mv-swatch-color:oklch(0.78 0.07 235)"><input type="checkbox" name="color" value="sky"><span class="mv-swatch-label">Sky</span></label>
<label class="mv-swatch-option" style="--mv-swatch-color:oklch(0.74 0.13 75)"><input type="checkbox" name="color" value="ochre"><span class="mv-swatch-label">Ochre</span></label>
<label class="mv-swatch-option" style="--mv-swatch-color:oklch(0.8 0.07 15)"><input type="checkbox" name="color" value="rose"><span class="mv-swatch-label">Dusty rose</span></label>
</div>
</fieldset>
</mv-swatch>
</div>
</details>
<details class="mv-facets-group" data-chip-prefix="Size">
<summary>Size</summary>
<div class="mv-facets-group-body">
<mv-swatch>
<fieldset class="mv-swatch" data-type="size" data-size="sm">
<legend class="mv-swatch-legend">Size</legend>
<div class="mv-swatch-options">
<label class="mv-swatch-option"><input type="checkbox" name="size" value="xs"><span class="mv-swatch-label">XS</span></label>
<label class="mv-swatch-option"><input type="checkbox" name="size" value="s"><span class="mv-swatch-label">S</span></label>
<label class="mv-swatch-option"><input type="checkbox" name="size" value="m"><span class="mv-swatch-label">M</span></label>
<label class="mv-swatch-option"><input type="checkbox" name="size" value="l"><span class="mv-swatch-label">L</span></label>
<label class="mv-swatch-option"><input type="checkbox" name="size" value="xl" disabled><span class="mv-swatch-label">XL</span></label>
</div>
</fieldset>
</mv-swatch>
</div>
</details>
<details class="mv-facets-group" open>
<summary>Price</summary>
<div class="mv-facets-group-body">
<div class="mv-facets-range">
<mv-slider name="price" label="Price" min="0" max="200" step="5" value="0,200"></mv-slider>
<div class="mv-facets-range-inputs">
<label data-unit="$"><span class="mv-sr-only">Minimum price</span><input type="number" inputmode="numeric" min="0" max="200" step="5" value="0"></label>
<span aria-hidden="true">–</span>
<label data-unit="$"><span class="mv-sr-only">Maximum price</span><input type="number" inputmode="numeric" min="0" max="200" step="5" value="200"></label>
</div>
</div>
</div>
</details>
<details class="mv-facets-group" data-deselectable>
<summary>Customer rating</summary>
<div class="mv-facets-group-body">
<div class="mv-facets-options">
<label class="mv-facets-option" data-chip="4 stars & up"><input type="radio" class="mv-radio" name="rating" value="4"><mv-rating readonly value="4" data-size="sm" label="Minimum rating"></mv-rating><span class="mv-facets-label">& up</span><span class="mv-facets-count">61</span></label>
<label class="mv-facets-option" data-chip="3 stars & up"><input type="radio" class="mv-radio" name="rating" value="3"><mv-rating readonly value="3" data-size="sm" label="Minimum rating"></mv-rating><span class="mv-facets-label">& up</span><span class="mv-facets-count">97</span></label>
</div>
</div>
</details>
<details class="mv-facets-group" open>
<summary>Availability</summary>
<div class="mv-facets-group-body">
<label class="mv-facets-option" data-chip="In stock"><span class="mv-facets-label">In stock only</span><input type="checkbox" role="switch" class="mv-switch" name="stock" value="1"></label>
</div>
</details>
</div>
<div class="mv-facets-footer">
<button type="button" class="mv-button" data-variant="outline" data-facets-clear>Clear</button>
<button type="submit" class="mv-button" data-facets-apply>Show products</button>
</div>
</form>
</mv-facets>
<section aria-labelledby="fc-demo-title">
<div class="bar">
<div>
<h2 id="fc-demo-title" style="margin:0;font-size:1.25rem;letter-spacing:-.01em">Fall new arrivals</h2>
<p id="fc-demo-count" style="margin:.125rem 0 0;font-size:.8125rem;color:var(--mv-fg-muted)" aria-live="polite">8 products</p>
</div>
<select class="mv-select" aria-label="Sort by" style="width:auto">
<option>Relevance</option><option>Price: low to high</option><option>Price: high to low</option><option>Newest</option>
</select>
</div>
<div class="grid" id="fc-demo-grid">
<article class="mv-product-card" data-cat="knitwear" data-color="sand" data-size="s m l" data-price="89" data-rating="4.6" data-stock="1"><div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.97 0.012 80)'/%3E%3Cstop offset='1' stop-color='oklch(0.91 0.025 75)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cdefs%3E%3Cpattern id='k' width='12' height='10' patternUnits='userSpaceOnUse'%3E%3Cpath d='M0 0l6 8 6-8' fill='none' stroke='black' stroke-opacity='.09' stroke-width='1.6'/%3E%3C/pattern%3E%3C/defs%3E%3Cellipse cx='200' cy='420' rx='150' ry='18' fill='url(%23fl)' opacity='1'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='oklch(0.8 0.05 75)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23k)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23sh)'/%3E%3Cpath d='M160 118 Q200 152 240 118 Q200 136 160 118Z' fill='black' fill-opacity='.22'/%3E%3Cpath d='M154 116 Q200 160 246 116' fill='none' stroke='black' stroke-opacity='.12' stroke-width='5'/%3E%3Cpath d='M133 370 Q200 379 267 370 L267 388 Q200 397 133 388 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M64 318 L100 331 L103 316 L68 303 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M336 318 L300 331 L297 316 L332 303 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M132 222 L133 380 M268 222 L267 380' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3C/svg%3E" alt=""></div><div class="mv-product-card-body"><h3 class="mv-product-card-title"><a class="mv-product-card-link" href="#">Merino crewneck sweater</a></h3><mv-price value="89" compare-at="112" discount="none" data-size="sm"></mv-price></div></article>
<article class="mv-product-card" data-cat="knitwear" data-color="navy" data-size="xs s m" data-price="99" data-rating="4.4" data-stock="1"><div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.95 0.012 250)'/%3E%3Cstop offset='1' stop-color='oklch(0.87 0.025 255)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cdefs%3E%3Cpattern id='k' width='12' height='10' patternUnits='userSpaceOnUse'%3E%3Cpath d='M0 0l6 8 6-8' fill='none' stroke='black' stroke-opacity='.09' stroke-width='1.6'/%3E%3C/pattern%3E%3C/defs%3E%3Cellipse cx='200' cy='420' rx='150' ry='18' fill='url(%23fl)' opacity='1'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='oklch(0.34 0.05 262)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23k)'/%3E%3Cpath d='M160 118 Q200 142 240 118 L276 128 Q300 140 310 176 L336 318 L300 331 L268 222 L267 388 Q200 397 133 388 L132 222 L100 331 L64 318 L90 176 Q100 140 124 128 Z' fill='url(%23sh)'/%3E%3Cpath d='M160 118 Q200 152 240 118 Q200 136 160 118Z' fill='black' fill-opacity='.22'/%3E%3Cpath d='M154 116 Q200 160 246 116' fill='none' stroke='black' stroke-opacity='.12' stroke-width='5'/%3E%3Cpath d='M133 370 Q200 379 267 370 L267 388 Q200 397 133 388 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M64 318 L100 331 L103 316 L68 303 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M336 318 L300 331 L297 316 L332 303 Z' fill='black' fill-opacity='.1'/%3E%3Cpath d='M132 222 L133 380 M268 222 L267 380' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3C/svg%3E" alt=""></div><div class="mv-product-card-body"><h3 class="mv-product-card-title"><a class="mv-product-card-link" href="#">Midnight merino sweater</a></h3><mv-price value="99" data-size="sm"></mv-price></div></article>
<article class="mv-product-card" data-cat="shoes" data-color="sky" data-size="" data-price="139" data-rating="4.8" data-stock="1"><div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.96 0.015 230)'/%3E%3Cstop offset='1' stop-color='oklch(0.88 0.035 235)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cellipse cx='200' cy='322' rx='170' ry='14' fill='url(%23fl)' opacity='1'/%3E%3Cg transform='translate(-10 10)'%3E%3Cpath d='M62 300 L358 300 C362 320 350 336 328 336 L86 336 C68 336 58 320 62 300 Z' fill='oklch(0.97 0.005 90)'/%3E%3Cpath d='M62 318 L360 318' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='oklch(0.78 0.07 235)'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='url(%23sh)'/%3E%3Cpath d='M292 240 C324 250 348 262 352 286 L354 300 L318 300 C318 278 306 256 292 240Z' fill='black' fill-opacity='.08'/%3E%3Cpath d='M140 292 C184 258 236 250 306 264' fill='none' stroke='white' stroke-opacity='.85' stroke-width='9' stroke-linecap='round'/%3E%3Cpath d='M214 206 l26 36 M226 200 l26 36 M200 214 l28 34' stroke='oklch(0.97 0.005 90)' stroke-width='5' stroke-linecap='round'/%3E%3Cpath d='M206 198 C226 186 252 188 264 204' fill='none' stroke='black' stroke-opacity='.18' stroke-width='5'/%3E%3Ccircle cx='96' cy='262' r='5' fill='white' fill-opacity='.8'/%3E%3C/g%3E%3C/svg%3E" alt=""></div><div class="mv-product-card-body"><h3 class="mv-product-card-title"><a class="mv-product-card-link" href="#">Aurora sneaker, sky</a></h3><mv-price value="139" data-size="sm"></mv-price></div></article>
<article class="mv-product-card" data-cat="shoes" data-color="brick" data-size="" data-price="139" data-rating="3.9" data-stock="0"><div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.96 0.015 45)'/%3E%3Cstop offset='1' stop-color='oklch(0.89 0.035 40)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cellipse cx='200' cy='322' rx='170' ry='14' fill='url(%23fl)' opacity='1'/%3E%3Cg transform='translate(-10 10)'%3E%3Cpath d='M62 300 L358 300 C362 320 350 336 328 336 L86 336 C68 336 58 320 62 300 Z' fill='oklch(0.97 0.005 90)'/%3E%3Cpath d='M62 318 L360 318' stroke='black' stroke-opacity='.08' stroke-width='3'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='oklch(0.58 0.13 38)'/%3E%3Cpath d='M68 300 C66 252 106 236 150 232 L206 198 C226 186 252 188 264 204 L292 240 C324 250 348 262 352 286 L354 300 Z' fill='url(%23sh)'/%3E%3Cpath d='M292 240 C324 250 348 262 352 286 L354 300 L318 300 C318 278 306 256 292 240Z' fill='black' fill-opacity='.08'/%3E%3Cpath d='M140 292 C184 258 236 250 306 264' fill='none' stroke='white' stroke-opacity='.85' stroke-width='9' stroke-linecap='round'/%3E%3Cpath d='M214 206 l26 36 M226 200 l26 36 M200 214 l28 34' stroke='oklch(0.97 0.005 90)' stroke-width='5' stroke-linecap='round'/%3E%3Cpath d='M206 198 C226 186 252 188 264 204' fill='none' stroke='black' stroke-opacity='.18' stroke-width='5'/%3E%3Ccircle cx='96' cy='262' r='5' fill='white' fill-opacity='.8'/%3E%3C/g%3E%3C/svg%3E" alt=""></div><div class="mv-product-card-body"><h3 class="mv-product-card-title"><a class="mv-product-card-link" href="#">Aurora sneaker, brick</a></h3><mv-price value="139" data-size="sm"></mv-price></div></article>
<article class="mv-product-card" data-cat="home" data-color="sage" data-size="" data-price="24.9" data-rating="4.7" data-stock="1"><div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.96 0.015 145)'/%3E%3Cstop offset='1' stop-color='oklch(0.89 0.03 150)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cellipse cx='200' cy='392' rx='130' ry='16' fill='url(%23fl)' opacity='1'/%3E%3Cpath d='M290 220 q64 0 64 60 q0 60-64 60' fill='none' stroke='oklch(0.7 0.06 150)' stroke-width='22' stroke-linecap='round'/%3E%3Cpath d='M110 170 L290 170 L284 370 Q200 392 116 370 Z' fill='oklch(0.86 0.03 75)'/%3E%3Cpath d='M110 170 L290 170 L287 270 C250 286 236 256 206 280 C176 300 150 266 113 284 Z' fill='oklch(0.7 0.06 150)'/%3E%3Cpath d='M110 170 L290 170 L284 370 Q200 392 116 370 Z' fill='url(%23sh)'/%3E%3Cellipse cx='200' cy='170' rx='90' ry='16' fill='oklch(0.36 0.05 55)'/%3E%3Cellipse cx='200' cy='170' rx='90' ry='16' fill='none' stroke='oklch(0.7 0.06 150)' stroke-width='6' style='filter:brightness(1.1)'/%3E%3C/svg%3E" alt=""></div><div class="mv-product-card-body"><h3 class="mv-product-card-title"><a class="mv-product-card-link" href="#">Glazed stoneware mug</a></h3><mv-price value="24.9" data-size="sm"></mv-price></div></article>
<article class="mv-product-card" data-cat="home" data-color="ochre" data-size="" data-price="129" data-rating="4.4" data-stock="1"><div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.97 0.02 85)'/%3E%3Cstop offset='1' stop-color='oklch(0.9 0.04 80)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cdefs%3E%3CradialGradient id='gb' cx='.38' cy='.34'%3E%3Cstop offset='0' stop-color='white'/%3E%3Cstop offset='1' stop-color='oklch(0.97 0.01 90)' stop-opacity='.85'/%3E%3C/radialGradient%3E%3C/defs%3E%3Cellipse cx='200' cy='424' rx='110' ry='12' fill='url(%23fl)' opacity='1'/%3E%3Ccircle cx='200' cy='210' r='118' fill='url(%23gb)'/%3E%3Ccircle cx='200' cy='210' r='118' fill='none' stroke='black' stroke-opacity='.06' stroke-width='2'/%3E%3Cpath d='M168 318 L232 318 L246 404 Q200 420 154 404 Z' fill='oklch(0.74 0.13 75)'/%3E%3Cpath d='M168 318 L232 318 L246 404 Q200 420 154 404 Z' fill='url(%23sh)'/%3E%3Crect x='160' y='312' width='80' height='12' rx='6' fill='oklch(0.72 0.1 80)'/%3E%3C/svg%3E" alt=""></div><div class="mv-product-card-body"><h3 class="mv-product-card-title"><a class="mv-product-card-link" href="#">Opaline lamp</a></h3><mv-price value="129" compare-at="159" discount="none" data-size="sm"></mv-price></div></article>
<article class="mv-product-card" data-cat="audio" data-color="rose" data-size="" data-price="189.9" data-rating="4.2" data-stock="0"><div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.97 0.012 20)'/%3E%3Cstop offset='1' stop-color='oklch(0.9 0.03 15)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cellipse cx='200' cy='392' rx='140' ry='14' fill='url(%23fl)' opacity='1'/%3E%3Cpath d='M112 270 C112 150 288 150 288 270' fill='none' stroke='oklch(0.8 0.07 15)' stroke-width='22' stroke-linecap='round'/%3E%3Cpath d='M112 270 C112 150 288 150 288 270' fill='none' stroke='white' stroke-opacity='.2' stroke-width='6' stroke-linecap='round' transform='translate(0 -6)'/%3E%3Crect x='80' y='250' width='70' height='112' rx='34' fill='oklch(0.8 0.07 15)'/%3E%3Crect x='250' y='250' width='70' height='112' rx='34' fill='oklch(0.8 0.07 15)'/%3E%3Crect x='80' y='250' width='70' height='112' rx='34' fill='url(%23sh)'/%3E%3Crect x='250' y='250' width='70' height='112' rx='34' fill='url(%23sh)'/%3E%3Crect x='134' y='262' width='22' height='88' rx='11' fill='oklch(0.3 0.01 270)'/%3E%3Crect x='244' y='262' width='22' height='88' rx='11' fill='oklch(0.3 0.01 270)'/%3E%3Ccircle cx='115' cy='306' r='9' fill='white' fill-opacity='.35'/%3E%3C/svg%3E" alt=""></div><div class="mv-product-card-body"><h3 class="mv-product-card-title"><a class="mv-product-card-link" href="#">Wave wireless headphones</a></h3><mv-price value="189.9" data-size="sm"></mv-price></div></article>
<article class="mv-product-card" data-cat="bags" data-color="ochre" data-size="" data-price="45" data-rating="4.5" data-stock="1"><div class="mv-product-card-media"><img class="mv-product-card-image" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 500'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='oklch(0.97 0.02 85)'/%3E%3Cstop offset='1' stop-color='oklch(0.9 0.04 80)'/%3E%3C/linearGradient%3E%3CradialGradient id='fl'%3E%3Cstop offset='0' stop-color='black' stop-opacity='.22'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0'/%3E%3C/radialGradient%3E%3ClinearGradient id='sh' x1='0' x2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='.22'/%3E%3Cstop offset='.55' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='.16'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='500' fill='url(%23bg)'/%3E%3Cellipse cx='200' cy='418' rx='140' ry='16' fill='url(%23fl)' opacity='1'/%3E%3Cpath d='M150 170 C150 100 250 100 250 170' fill='none' stroke='oklch(0.62 0.06 60)' stroke-width='12' stroke-linecap='round'/%3E%3Cpath d='M168 172 C168 118 232 118 232 172' fill='none' stroke='oklch(0.55 0.06 60)' stroke-width='10' stroke-linecap='round'/%3E%3Cpath d='M102 170 L298 170 L318 410 Q200 420 82 410 Z' fill='oklch(0.74 0.13 75)'/%3E%3Cpath d='M102 170 L298 170 L318 410 Q200 420 82 410 Z' fill='url(%23sh)'/%3E%3Crect x='140' y='150' width='20' height='40' rx='4' fill='oklch(0.55 0.06 60)'/%3E%3Crect x='240' y='150' width='20' height='40' rx='4' fill='oklch(0.55 0.06 60)'/%3E%3Ctext x='200' y='300' text-anchor='middle' font-family='Georgia,serif' font-size='30' letter-spacing='8' fill='black' fill-opacity='.55'%3ELUMEN%3C/text%3E%3Ctext x='200' y='326' text-anchor='middle' font-family='Arial,sans-serif' font-size='10' letter-spacing='4' fill='black' fill-opacity='.45'%3ESTUDIO %C2%B7 LONDON%3C/text%3E%3Cpath d='M102 170 L298 170' stroke='black' stroke-opacity='.15' stroke-width='6'/%3E%3C/svg%3E" alt=""></div><div class="mv-product-card-body"><h3 class="mv-product-card-title"><a class="mv-product-card-link" href="#">Canvas tote bag</a></h3><mv-price value="45" data-size="sm"></mv-price></div></article>
</div>
<p class="empty" id="fc-demo-empty" hidden>No products match. Try removing a filter.</p>
</section>
</div>
</div>
<script type="module">
const facets = document.getElementById("fc-demo-facets");
const cards = [...document.querySelectorAll("#fc-demo-grid > article")];
const count = document.getElementById("fc-demo-count");
const empty = document.getElementById("fc-demo-empty");
// In an app: send e.detail.filters to the search API. Here we filter locally.
facets.addEventListener("mv-change", (e) => {
const f = e.detail.filters;
let n = 0;
for (const c of cards) {
const d = c.dataset;
const ok = (!f.category || f.category.includes(d.cat))
&& (!f.color || f.color.includes(d.color))
&& (!f.size || f.size.some((t) => d.size.split(" ").includes(t)))
&& (!f.price || (Number(d.price) >= f.price[0] && Number(d.price) <= f.price[1]))
&& (!f.rating || Number(d.rating) >= Number(f.rating))
&& (!f.stock || d.stock === "1");
c.hidden = !ok;
if (ok) n++;
}
facets.resultCount = n;
count.textContent = `${n} product${n === 1 ? "" : "s"}`;
empty.hidden = n > 0;
});
</script>
API
Attributes
Name Type Default Description result-countnumber Result count: drives the [data-facets-apply] button label (“Show 42 products”, disabled at 0). modeauto | drawer | inline autoauto: inline panel from a 64rem viewport, drawer below; drawer: always a drawer; inline: always inline. currency / localestring USD / en-USFormatting of price chips. labelstring FiltersLabel of the open button and the drawer. data-limitnumber On .mv-facets-group: options visible before “Show n more”. data-deselectableboolean On a group or a radio: clicking again unchecks it. data-chip-prefix / data-chipstring Chip prefix for the group (“Size M”); explicit chip label on an option. data-formatmoney | number moneyOn mv-slider: format of the tooltip and the chip.
Properties
Name Type Description filtersobject { category: ['knitwear'], price: [20, 150], rating: '4', … } (read-only). activeCountnumber Number of active filters.
Methods
Name Description open() / close()Mobile drawer. clear()Resets every filter (emits mv-change).
Events
Name Description mv-changeDebounced (120 ms); detail: { filters, count }. Inner mv-change events (slider, swatches) don’t bubble out. mv-open / mv-closeDrawer.
Content structure
Name Description form.mv-facetsPanel: .mv-facets-header (.mv-facets-title), .mv-facets-body (groups), .mv-facets-footer (visible in the drawer). details.mv-facets-group > summary + .mv-facets-group-bodyGroup; .mv-facets-options > label.mv-facets-option (input, .mv-facets-label, .mv-facets-count), mv-swatch, .mv-facets-range. [data-facets-open] / [data-facets-chips]Custom open button and chip area (generated otherwise). [data-facets-clear] / [data-facets-apply] / [data-facets-close]Actions.
CSS classes
Name Description mv-facets-trigger / -dialog / -header / -title / -close / -body / -footer / -active / -chip / -chip-remove / -clear / -group / -group-count / -options / -option / -label / -count / -more / -range / -range-inputsStructure.
Accessibility
Groups use native <details>/<summary> with an announced selection count (“2 selected”). Native checkboxes, radios and swatches (a real form). Price range: two role=slider thumbs (tooltip formatted as currency) backed by two labeled number inputs. The chip area is an “Active filters” region; each × reads “Remove filter Color: Sage”, focus moves to the neighboring chip, and removals are announced (role=status). Drawer: modal <dialog> (focus trap, Esc, backdrop click); focus returns to the “Filters” button, which states the number of active filters.