Set filter

A "set filter" (a.k.a. value filter, list filter) shows a checklist of all distinct values in a column and lets the user pick which to include. It's what you reach for to filter status to "active OR pending", or department to a few specific teams.

An Excel-style set filter: a per-column checklist of distinct values with a Select all row, search, and a facet count, applying an IN filter to the grid.

Open the live example: Set filter (tree / async / Excel) (Filtering & Search)

Three patterns are supported, all wired through the imperative api.setFacetFilter(columnId, values | null):

Mode UI Built-in? When to use
Excel-style Column-menu Values tab Yes Most columns. Distinct values are enumerated from the loaded data; search box + select-all + clear ship out of the box.
Async Side panel that loads values from a server endpoint User-land (one screen of code) The column has too many distinct values to list at full data load. Lazy-fetch on demand.
Tree-list Hierarchical checkboxes (parent ↔ descendants) User-land (cascade logic) Nested taxonomies: Region → Country → City; Department → Team → Employee.

1. Excel-style (built-in)

The column menu's Values tab is the default set filter. Click the funnel icon on any header to open it.

What you get without writing any code:

Programmatic equivalent for "remember and restore":

// Capture the current set
const filters = api.getFilters() // includes selectedValues per column

// Restore later (e.g. saved view, URL persistence)
api.setFacetFilter('status', ['active', 'pending'])
api.setFacetFilter('status', null) // clear

2. Async values (server-loaded)

When a column has tens of thousands of distinct values, you don't want to pre-render them all. Pattern: render a side panel beside the grid, load values from the server on first open, drive the grid via api.setFacetFilter:

let state = $state<{
  status: 'idle' | 'loading' | 'ready' | 'error'
  values: string[]
}>({ status: 'idle', values: [] })
let selected = $state<Set<string>>(new Set())

async function loadValues() {
  state = { status: 'loading', values: [] }
  const res = await fetch('/api/orders/customers')
  const values = await res.json()
  state = { status: 'ready', values }
}

function toggle(value: string) {
  const next = new Set(selected)
  if (next.has(value)) next.delete(value); else next.add(value)
  selected = next
  api.setFacetFilter('customer', next.size === 0 ? null : Array.from(next))
}

Key benefits vs the Excel tab:

See demo 111 ("Async values" card) for a complete implementation with loading state, retry, and search.

3. Tree-list (hierarchical)

For nested taxonomies, render a tree of checkboxes. Parent checked = all descendants checked. Some descendants checked = parent shows the "indeterminate" state. On any change, compute the leaf set and apply it to the column.

// Taxonomy: Region → Country → City
const GEO = {
  Americas: { 'United States': ['New York', 'San Francisco'], Canada: ['Toronto'] },
  EMEA:     { Germany: ['Berlin', 'Munich'], France: ['Paris'] },
}

let selectedCities = $state<Set<string>>(new Set())

function toggleNode(node: TreeNode, on: boolean) {
  const next = new Set(selectedCities)
  for (const city of node.cities) {  // pre-computed leaf set
    if (on) next.add(city); else next.delete(city)
  }
  selectedCities = next
  api.setFacetFilter('city', next.size === 0 ? null : Array.from(next))
}

function isChecked(node: TreeNode): boolean {
  return node.cities.every((c) => selectedCities.has(c))
}
function isPartial(node: TreeNode): boolean {
  const hits = node.cities.filter((c) => selectedCities.has(c)).length
  return hits > 0 && hits < node.cities.length
}

The grid is unaware of the hierarchy - it just receives a flat list of allowed leaf values. The hierarchy lives in your panel UI.

See demo 111 ("Tree" card) and demo 102: Tree checkbox cascade for the cascade-logic recipe.

API surface

type SvGridApi<…> = {
  // Set a multi-select set filter. Pass null or [] to clear.
  setFacetFilter(columnId: string, values: ReadonlyArray<string> | null): void

  // Read the current filters (includes the facet selection per column).
  getFilters(): Record<string, { operator: ..., selectedValues?: string[] }>

  // Snapshot of the rows the grid currently displays - useful when your
  // filter UI needs to count matches without re-running the search.
  getDisplayedRows(): ReadonlyArray<TData>
}

Filtering to a set of values

A set filter is a checklist of the distinct values in the column, which is the right shape when the domain is small and closed - a department, a status, a country. Typing an operator for those is slower than ticking a box.

Three things the checklist does on its own:

<script lang="ts">
  import { SvGrid, renderSnippet, type GridColumns, type SvGridApi } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    age: number
    salary: number
    active: boolean
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   age: 36, salary: 142000, active: true },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', age: 45, salary: 168000, active: true },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', age: 54, salary: 155000, active: false },
    { id: 4, name: 'Radia Perlman',  department: 'Networking',  city: 'Seattle',  age: 49, salary: 161000, active: true },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 180 },
    { field: 'department', header: 'Department', width: 170 },
    { field: 'city',       header: 'City',       width: 150 },
  ]
</script>

<SvGrid data={people} {columns} filterable filterMode="menu" />

Building the value list yourself

When the choices should come from your domain rather than from whatever happens to be loaded, derive them and filter the array. A set the user can see but not currently reach is often the point - it tells them the value exists.

<script lang="ts">
  import { SvGrid, renderSnippet, type GridColumns, type SvGridApi } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    age: number
    salary: number
    active: boolean
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   age: 36, salary: 142000, active: true },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', age: 45, salary: 168000, active: true },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', age: 54, salary: 155000, active: false },
    { id: 4, name: 'Radia Perlman',  department: 'Networking',  city: 'Seattle',  age: 49, salary: 161000, active: true },
  ]

  const DEPARTMENTS = ['Engineering', 'Platform', 'Networking', 'Design'] as const

  let picked = $state<string[]>([])

  const shown = $derived(
    picked.length === 0 ? people : people.filter((p) => picked.includes(p.department)),
  )

  function toggle(d: string) {
    picked = picked.includes(d) ? picked.filter((x) => x !== d) : [...picked, d]
  }

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 180 },
    { field: 'department', header: 'Department', width: 170 },
  ]
</script>

<div>
  {#each DEPARTMENTS as d}
    <label><input type="checkbox" checked={picked.includes(d)} onchange={() => toggle(d)} /> {d}</label>
  {/each}
</div>

<SvGrid data={shown} {columns} />

See also

Live examples

  • Set filter (tree / async / Excel) - Three set-list filter patterns: the built-in Excel-style column menu, async-loaded values for huge enums, and a tree-list (Region → Country → City) with cascading checkboxes. All driven through api.setFacetFilter.
  • Tree checkbox cascade - Permissions matrix as a tree: workspaces → resources → actions. Checking a parent checks descendants; partial state on parents. Live cost rollup.

Related articles