Product Filters
A side panel of product filters, built from a config so the same panel works for any catalog. Each filter is a section that opens and closes, with its own reset; the active filters show as chips under the header, with "Clear all". value is bindable and holds one entry per active filter, and onchange reports every change; the panel fetches nothing. Give it a height: the header stays put while the sections scroll. For a phone, pair it with ProductFiltersSheet, hiding the panel below the breakpoint with hidden md:flex (the panel sets flex itself, and hidden wins over it). Filter types: checkbox (a list with counts, search and "Show more"), radio (one choice), range (a two-handle slider with linked boxes, and a histogram if you pass one), rating (stars and up), swatch (colours) and switch (on/off options).
The whole panel
One section per filter in the config, each with its own Reset once it holds a choice, and the active filters as chips under the header with Clear all. Give it a height: the header stays put while the sections scroll.
Filters
{}<div class="grid gap-4 md:grid-cols-[18rem_1fr]">
<ProductFilters
{config}
bind:value={full}
class="surface-sheen h-136 rounded-lg py-3"
/>
<pre class="text-muted text-xs">{JSON.stringify(full, null, 2)}</pre>
</div>Sections collapsed and expanded
Every section starts open. Bind open to the ids of the open sections to start some closed, or to remember them.
Filters
<ProductFilters
{config}
bind:value={collapsedValue}
bind:open={closed}
class="surface-sheen h-96 w-72 rounded-lg py-3"
/>Active chips
Ticked values get a chip each; a range, a rating and a single choice get one chip for the whole filter. Removing a chip removes only that choice.
Filters
4<ProductFilters
{config}
bind:value={chips}
class="surface-sheen h-120 w-72 rounded-lg py-3"
/>Checkbox list
A list with counts. Past five options the rest wait behind Show more, and past eight a search box appears.
<div class="grid gap-8 md:grid-cols-2">
<div class="w-64">
<ProductFiltersCheckboxList
filter={{
id: 'brand',
label: 'Brand',
options: BRANDS,
counts: { Glam: 4, Luxe: 3, Nova: 2, Aura: 1, Bloom: 2 }
}}
value={brand}
onchange={(next) => (brand = next)}
/>
</div>
<div class="w-64">
<ProductFiltersCheckboxList
filter={{ id: 'shade', label: 'Shade', options: shades, counts: shadeCounts }}
value={shade}
onchange={(next) => (shade = next)}
/>
</div>
</div>Zero-count options
An option that would leave nothing is disabled, unless it is already chosen, so it can always be taken back. Here Tools and Clear have no products left.
<div class="flex flex-wrap gap-8">
<div class="w-56">
<ProductFiltersRadio
filter={{
id: 'category',
label: 'Category',
options: CATEGORIES,
counts: { polish: 4, care: 3, tools: 0, kits: 2 }
}}
value={category}
onchange={(next) => (category = next)}
/>
</div>
<div class="w-56">
<ProductFiltersSwatches
filter={{
id: 'colour',
label: 'Colour',
options: COLOURS,
counts: { rose: 3, coral: 1, nude: 3, ink: 2, clear: 0 }
}}
value={colours}
onchange={(next) => (colours = next)}
/>
</div>
</div>Loading counts
loadingCounts swaps the numbers for placeholders while new counts are on their way. The options stay where they are and nothing is disabled, so the panel doesn't flicker on every change.
Filters
<ProductFilters {config} loadingCounts class="surface-sheen h-80 w-72 rounded-lg py-3" />No options
A filter with nothing to choose says so, rather than showing an empty section.
Filters
No options
<ProductFilters
config={[{ id: 'brand', type: 'checkbox', label: 'Brand', options: [] }]}
class="surface-sheen w-72 rounded-lg py-3"
/>Price range
Two handles with a box for each end. Dragging shows the numbers as they move and sets the filter when the handle is let go; a typed number is set on Enter, put in order and kept on the scale. Pass histogram, a count per equal slice of the scale, and bars above the slider show where the products are, lit inside the range — so the shopper can see a cut-off before dragging to it.
$20 – $80
<div class="w-72 space-y-2">
<ProductFiltersRange
filter={{
id: 'price',
label: 'Price',
min: 0,
max: 150,
step: 5,
format: money,
histogram
}}
value={price}
onchange={(next) => (price = next)}
/>
<p class="text-muted text-xs tabular-nums">${price[0]} – ${price[1]}</p>
</div>Rating and on/off options
A rating filter means that many stars and up. On/off options, such as In stock, each switch on their own.
<div class="flex flex-wrap gap-8">
<ProductFiltersRating
filter={{ id: 'rating', label: 'Rating' }}
value={rating}
onchange={(next) => (rating = next)}
/>
<div class="w-56">
<ProductFiltersSwitches
filter={{
id: 'perks',
label: 'Availability',
options: PERKS,
counts: { stock: 10, shipping: 7 }
}}
value={perks}
onchange={(next) => (perks = next)}
/>
</div>
</div>Mobile sheet
On a phone the filters open in a sheet from a Filters button that shows how many are active. Choices made there are a draft until Apply; Cancel drops them. Pair it with the panel by breakpoint: md:hidden here, hidden md:flex on the panel.
Applied: {"category":"polish"}
<div class="space-y-2">
<ProductFiltersSheet {config} bind:value={mobile} />
<p class="text-muted text-xs">Applied: {JSON.stringify(mobile)}</p>
</div>API
ProductFilters
A side panel of product filters, built from a config so the same panel works for any catalog. Each filter is a section that opens and closes, with its own reset; the active filters show as chips under the header, with "Clear all". value is bindable and holds one entry per active filter, and onchange reports every change; the panel fetches nothing. Give it a height: the header stays put while the sections scroll. For a phone, pair it with ProductFiltersSheet, hiding the panel below the breakpoint with hidden md:flex (the panel sets flex itself, and hidden wins over it). Filter types: checkbox (a list with counts, search and "Show more"), radio (one choice), range (a two-handle slider with linked boxes, and a histogram if you pass one), rating (stars and up), swatch (colours) and switch (on/off options).
| Prop | Type | Default | Description |
|---|---|---|---|
config required | ProductFilter[] | The filters, in the order to show them. | |
value bindable | Object<string, *> | {} | One entry per active filter: a list of values for |
onchange | (value: Object<string, *>) => void | Called with the new value after every change. | |
loadingCounts | boolean | false | Shows placeholders in place of the counts while they load, and disables nothing. |
open bindable | string[] | config.map((filter) => filter.id) | The ids of the open sections; every section by default. Two-way bindable via |
class | string | Additional Tailwind classes merged onto the panel. Give it a height. |
ProductFiltersSheet
The product filters for a phone: a "Filters" button, with the number of active filters, that opens the panel in a sheet. Choices made in the sheet are a draft until Apply, which sets value and calls onchange once; Cancel or closing the sheet drops them. Show it below a breakpoint and the ProductFilters panel above it, such as class="md:hidden" here and class="hidden md:flex" there.
| Prop | Type | Default | Description |
|---|---|---|---|
config required | import('./product-filters.svelte').ProductFilter[] | The filters, in the order to show them. | |
value bindable | Object<string, *> | {} | The applied filters, in the same shape as |
onchange | (value: Object<string, *>) => void | Called once with the new value when Apply is pressed. | |
loadingCounts | boolean | false | Shows placeholders in place of the counts while they load. |
class | string | Additional Tailwind classes merged onto the button. |
ProductFiltersCheckboxList
A filter as a list of checkboxes, each with its count. Long lists show the first few with "Show more", and a search box appears once there are enough options to need one. An option whose count is zero is disabled, unless it is already ticked, so it can always be unticked.
| Prop | Type | Default | Description |
|---|---|---|---|
filter required | {id: string, label: string, options: Array<string|{label: string, value: string}>, counts?: Object<string, number>} | The filter's config. | |
value | string[] | [] | The ticked values. |
onchange required | (next: string[]) => void | Called with the ticked values after each tick. | |
loadingCounts | boolean | false | Shows placeholders in place of the counts, and disables nothing. |
ProductFiltersRadio
A filter where one option is chosen, such as a category, with a count beside each. An option whose count is zero is disabled unless it is the one chosen.
| Prop | Type | Default | Description |
|---|---|---|---|
filter required | {id: string, label: string, options: Array<string|{label: string, value: string}>, counts?: Object<string, number>} | The filter's config. | |
value | string | '' | The chosen value. |
onchange required | (next: string) => void | Called with the newly chosen value. | |
loadingCounts | boolean | false | Shows placeholders in place of the counts, and disables nothing. |
ProductFiltersRange
A filter for a range of numbers, such as price: a two-handle slider with a box for each end. Dragging shows the numbers as they move and sets the filter once the handle is let go; a typed number is set on Enter or when the box loses focus, put in order and kept on the scale. With a histogram, bars above the slider show how the products spread across the scale, and light up inside the range.
| Prop | Type | Default | Description |
|---|---|---|---|
filter required | {id: string, label: string, min: number, max: number, step?: number, format?: (number: number) => string, histogram?: number[]} | The filter's config. | |
value | [number, number] | The chosen range; the whole scale when not set. | |
onchange required | (next: [number, number]) => void | Called with the range, in order and on the scale. |
ProductFiltersRating
A filter for a minimum rating: choosing four stars means four stars and up.
| Prop | Type | Default | Description |
|---|---|---|---|
filter required | {id: string, label: string, max?: number} | The filter's config. | |
value | number | 0 | The minimum rating; none when not set. |
onchange required | (next: number) => void | Called with the newly chosen minimum. |
ProductFiltersSwatches
A filter for colours, as round swatches that toggle. Several can be chosen. A colour whose count is zero is disabled unless it is already chosen.
| Prop | Type | Default | Description |
|---|---|---|---|
filter required | {id: string, label: string, options: Array<{label: string, value: string, color: string}>, counts?: Object<string, number>} | The filter's config. | |
value | string[] | [] | The chosen colours' values. |
onchange required | (next: string[]) => void | Called with the chosen values after each toggle. | |
loadingCounts | boolean | false | Disables nothing while the counts load. |
ProductFiltersSwitches
A filter made of independent on/off options, such as "In stock" and "Free shipping", each with its count. An option whose count is zero is disabled unless it is already on.
| Prop | Type | Default | Description |
|---|---|---|---|
filter required | {id: string, label: string, options: Array<string|{label: string, value: string}>, counts?: Object<string, number>} | The filter's config. | |
value | string[] | [] | The options that are on. |
onchange required | (next: string[]) => void | Called with the options that are on after each switch. | |
loadingCounts | boolean | false | Shows placeholders in place of the counts, and disables nothing. |
ProductFilter
| Prop | Type | Default | Description |
|---|---|---|---|
id required | string | Key of the filter in | |
type required | 'checkbox'|'radio'|'range'|'rating'|'swatch'|'switch' | How the filter is chosen. | |
label required | string | Section title, also used in the chips. | |
options | Array<string|{label: string, value: string, color?: string}> | What can be chosen; | |
counts | Object<string, number> | How many products each option would leave; zero disables the option. | |
min | number | Lowest value of a | |
max | number | Highest value of a | |
step | number | Step of a | |
format | (number: number) => string | How a | |
histogram | number[] | How many products fall in each equal slice of a |