FilterPanel
Pre-styled collapsible shell for index filters: icon, summary, and chevron trigger with staggered panel content.
Usage
svelte
<script lang="ts">
import {
FilterChipGroup,
FilterPanel,
FilterSortRow,
toggleCounts,
} from "@milandekruijf/ui/svelte";
const items = [
{ title: "Static i18n plumbing", tags: ["sveltekit", "i18n"] },
{ title: "Prerendered sitemap", tags: ["sveltekit", "seo"] },
];
const options = ["i18n", "seo", "sveltekit"];
let open = $state(true);
let selected = $state<string[]>(["seo"]);
let sort = $state("newest");
const counts = $derived(toggleCounts(items, (i) => i.tags, selected, options));
const visible = $derived(items.filter((i) => selected.every((t) => i.tags.includes(t))));
const summary = $derived(
selected.length === 0 ? "Filter & Sort" : `Filtered by ${selected.length} tags`,
);
</script>
<FilterPanel bind:open {summary}>
<FilterChipGroup
title="Tags"
{options}
bind:value={selected}
counts={counts.counts}
resultCount={visible.length}
/>
<FilterSortRow
sortTitle="Sort by"
bind:sortValue={sort}
sortOptions={[{ value: "newest", label: "Newest first" }]}
showReset={selected.length > 0}
resetLabel="Reset filters"
onReset={() => (selected = [])}
/>
</FilterPanel>Examples
Full filter panel
Tags
Sort by
- Static i18n plumbing
- Prerendered sitemap
<script lang="ts">
import { css } from "@milandekruijf/pandacss/css";
import {
FilterChipGroup,
FilterPanel,
FilterSortRow,
toggleCounts,
} from "@milandekruijf/ui/svelte";
const items = [
{ title: "Static i18n plumbing", tags: ["sveltekit", "i18n"] },
{ title: "Prerendered sitemap", tags: ["sveltekit", "seo"] },
{ title: "Adapter-static notes", tags: ["static-site"] },
];
const options = ["i18n", "seo", "static-site", "sveltekit"];
let open = $state(true);
let selected = $state<string[]>(["sveltekit"]);
let sort = $state("newest");
const counts = $derived(
toggleCounts(items, (item) => item.tags, selected, options),
);
const visible = $derived(
items.filter((item) => selected.every((tag) => item.tags.includes(tag))),
);
const summary = $derived(
selected.length === 0
? "Filter & Sort"
: `Filtered by ${selected.length} tag${selected.length > 1 ? "s" : ""}`,
);
function reset() {
selected = [];
sort = "newest";
}
</script>
<div class={css({ display: "flex", flexDirection: "column", gap: 4, minW: 0 })}>
<FilterPanel bind:open {summary}>
<FilterChipGroup
title="Tags"
{options}
bind:value={selected}
counts={counts.counts}
resultCount={visible.length}
/>
<FilterSortRow
sortTitle="Sort by"
bind:sortValue={sort}
sortOptions={[
{ value: "newest", label: "Newest first" },
{ value: "oldest", label: "Oldest first" },
]}
showReset={selected.length > 0 || sort !== "newest"}
resetLabel="Reset filters"
onReset={reset}
/>
</FilterPanel>
{#if visible.length > 0}
<ul class={css({ m: 0, p: 0, listStyle: "none" })}>
{#each visible as item (item.title)}
<li class={css({ color: "slate.12", py: 1 })}>{item.title}</li>
{/each}
</ul>
{/if}
</div>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| open? | boolean | false | Bindable open state (`bind:open`). |
| summary? | string | "Filter & Sort" | Default trigger text, e.g. “Filtered by 2 tags”; ignored when `trigger` is set. |
| trigger? | Snippet | — | Replaces the default filter-icon + summary + chevron trigger. |
| children | Snippet | — | Chip groups and the sort row inside the panel; toggle items stagger on open. |
| class? | string | — | Extra classes merged on the collapsible root. |
| triggerClass? | string | — | Extra classes merged on the trigger button. |
A pre-styled `Collapsible` for index filters: hairline top border, filter trigger, and the same height + stagger motion — toggle chips inside animate in sequence automatically.