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

PropTypeDefaultDescription
open?booleanfalse
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.
childrenSnippet—
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.