---
title: Filter
description: Use Filter to help people narrow down a list or data set.
version: 11.17.0
generatedAt: 2026-11-05T09:55:46.144Z
checksum: 9b7172ed79a87132728698d9333f2da14362572b23d106cba49138313f731c4f
---

# Filter

## Import

```tsx
import { Filter } from '@dnb/eufemia'
```

## Description

`Filter` is a composable filter UI for building search and filter experiences. It does **not** own your data — instead, it provides shared state that you read with the `Filter.useFilter(id)` hook and apply to your own data source.

The component uses a namespace pattern where `Filter` is the import and `Filter.Root` is the renderable root.

### Behavior

By default, `Filter.Root` emits changes in real time via `onChange`. Set `behavior="manual"` to buffer filter changes internally — the panel will show an "Apply filter" button to commit changes and a "Cancel" button to revert changes. This is useful when filter changes trigger expensive operations like API calls. Note that search input is always emitted in real time, even in manual mode. `Filter.ActiveFilters` only shows applied filters, so tags won't appear until the user applies them.

### Filter keys

Each filter is identified by a `filterKey` string. For `Filter.Selection` and `Filter.MultiSelection`, individual selected values are stored as `{filterKey}/{value}` entries in the state (e.g. `/status/active`, `/status/inactive`). This convention lets you inspect which values are selected by filtering the keys that start with the filter's prefix:

```ts
const selectedStatuses = Object.keys(filters)
  .filter((key) => key.startsWith('/status/'))
  .map((key) => key.replace('/status/', ''))
```

The leading `/` is a convention to namespace filter keys — it is not a URL path or an Eufemia Forms JSON Pointer. You can use any string as a `filterKey`, but we recommend starting with `/` for consistency.

When building custom filters with `Filter.useFilterContext()`, you can use any key format — the `{filterKey}/{value}` pattern is only used by the built-in selection components.

### Combining search and filters

The Filter component stores `search` and `filters`, but does not decide how your data should match them. As a rule of thumb, each active filter should narrow the result set.

Different filter groups are also usually combined with **AND**, such as status and region. Multiple values inside the same filter group usually behave as **OR**. For example, selecting two statuses means the item can match status active or inactive. Search can also match several fields with **OR**, such as name or amount.

For custom filters and quick filters, choose the logic that matches the meaning of the controls. Use **OR** when the buttons are alternatives in the same category, and **AND** when they represent independent conditions that should all be true.

### Layout

The Filter component can be used in two layout patterns:

- **Inside a list** — Place `Filter.Root` inside the first `List.Item.Basic`. Filtered results render as subsequent list items. This is the most common pattern.
- **Column layout with Grid** — Use `Grid.Container` with `Grid.Item` to place the filter and results side by side. Use `Filter.Content` to link the results area to the filter via `connectedTo`.

### Sub-components

- **`Filter.Root`** — Root wrapper. Provides filter context and syncs state via `useSharedState`. Requires a unique `id`. Supports spacing props.
- **`Filter.Header`** — Groups the filter controls (toolbar, panel, active filters) above the results. When used together with `Filter.Content` containing a `List.Container`, the header receives a subtle background and top border-radius to visually connect with the list below.
- **`Filter.Search`** — Text input with a loupe icon. Updates the shared `search` string. Browser autocomplete, autocorrect, autocapitalize, and spellcheck are disabled by default.
- **`Filter.Toolbar`** — Horizontal row that wraps `Filter.Search` and `Filter.Toolbar.Actions`.
- **`Filter.Toolbar.Actions`** — Groups action buttons (e.g. `Filter.PanelButton`) for proper responsiveness.
- **`Filter.Panel`** — Expandable inline panel toggled by `Filter.PanelButton`. Renders filter children as tertiary accordions with a white background.
- **`Filter.PanelButton`** — Toggle button for `Filter.Panel`. Shows a filter icon when closed and a close icon when open. Accepts all `Button` props.
- **`Filter.ActiveFilters`** — Renders active filters as removable `Tag` chips. Returns nothing when no filters are active. Set `showCategoryLabel` to prefix each tag with its category name (e.g. "Betalingstype: Kort" instead of "Kort"). Set `collapsibleThreshold` to collapse the tags behind a tertiary accordion with a scrollable area and a "Clear all" button when the number of active filters exceeds the threshold. In `behavior="manual"` mode, only applied filters are shown — draft changes in the panel won't appear as tags until the user clicks Apply.
- **`Filter.Item`** — Accordion wrapper for a single filter section. Supports `defaultOpen` to start expanded. Open/closed state is remembered across panel opens.
- **`Filter.Date`** — Built-in date range filter using `DatePicker`. When placed inside a `Filter.Panel`, it renders as an accordion with an inline calendar on larger screens. On small screens, it skips the accordion and renders as a tertiary trigger button that opens a calendar popover. When placed outside the panel, it always renders as a trigger button.
- **`Filter.Selection`** — Built-in checkbox selection filter. Each selected option creates its own active filter tag.
- **`Filter.MultiSelection`** — Built-in multi-selection filter using the Forms `MultiSelection` component. Each selected item creates its own active filter tag.
- **`Filter.SortButton`** — Sort dropdown styled as a tertiary button with a sort icon. The trigger always displays the translated "Sort" label regardless of the selected option.
- **`Filter.QuickFilters`** — Wrapper for quick filter toggle buttons placed outside the panel. Renders as a horizontal flex row with wrapping.
- **`Filter.Highlighting`** — Highlights matching search text within result items. Reads the `search` string from the linked filter state and wraps matching substrings in `<mark>` tags. Can be linked via `connectedTo` or inherits the id from the nearest `Filter.Root` or `Filter.Content`.
- **`Filter.Content`** — Wraps result content and shows a `Skeleton` loading state when the filter is loading. When used inside a `Filter.Root`, the id is inherited automatically. When used outside, link it via `connectedTo`. Supports spacing props.
- **`Filter.NoResults`** — Renders a translated "no results" message when `resultCount` is `0`. When placed inside a `List.Container`, it automatically renders as a list item. Can be placed after `Filter.ActiveFilters` inside a container, or inside a `Filter.Content` where it inherits `connectedTo` automatically.
- **`Filter.ResultCount`** — Displays the current result count as a translated message (e.g. "3 result(s)") when filters are active. Hidden when no filters or search text are applied. Reads `resultCount` from the nearest `Filter.Root`, a `connectedTo` id, or a `resultCount` prop. Supports spacing props.

### Hooks

- **`Filter.useFilter(id)`** — Reads filter state from anywhere — does not need to be inside `Filter.Root`. Returns `{ filters, search, hasActiveFilters, resetFilters, removeFilter }`.
- **`Filter.useFilterAsync(id, fetcher, options?)`** — Async data fetching linked to a filter. Handles loading state, race conditions, and syncs `resultLoading`/`resultCount` to shared state. Options: `initialData` for immediate rendering, `debounce` (ms) to delay fetcher calls while the user is typing. Returns `{ data, loading, error }`.
- **`Filter.useFilterContext()`** — Accesses the full filter context from inside `Filter.Root`. Use this to build custom filter types. Returns `{ setFilter, getFilter, removeFilter, resetFilters, commitFilters, revertFilters, filters, search, hasActiveFilters }`.

### URL sync hooks

These hooks sync filter state to URL query parameters so filters survive page reloads and browser navigation. Each hook writes `{id}-search` and `{id}-filters` query parameters. Pass `excludeSearch: true` to skip syncing the search string.

- **`Filter.useQueryLocator(id, options?)`** — Uses the History API directly. Works without any router. Best for plain React apps or when no router is available.
- **`Filter.useReactRouter(id, { useSearchParams, ...options })`** — Uses React Router's `useSearchParams`. Pass the hook from your router version.
- **`Filter.useNextRouter(id, { useRouter, usePathname, useSearchParams, ...options })`** — Uses Next.js App Router hooks. Pass `useRouter`, `usePathname`, and `useSearchParams` from `next/navigation`.

## Basic usage

```tsx
import { Filter, List } from '@dnb/eufemia'

function MyPage() {
  const { filters, search, hasActiveFilters } =
    Filter.useFilter('my-filter')

  const filtered = myData.filter((item) => {
    if (search && !item.name.includes(search)) {
      return false
    }
    return true
  })

  return (
    <List.Container>
      <List.Item.Basic>
        <Filter.Root id="my-filter" resultCount={filtered.length}>
          <Filter.Toolbar>
            <Filter.Search label="Søk" placeholder="Søk ..." />
            <Filter.Toolbar.Actions>
              <Filter.Date />
              <Filter.PanelButton />
            </Filter.Toolbar.Actions>
          </Filter.Toolbar>
          <Filter.Panel>
            <Filter.Selection
              label="Status"
              filterKey="/status"
              data={[
                { value: 'active', label: 'Aktiv' },
                { value: 'inactive', label: 'Inaktiv' },
              ]}
            />
          </Filter.Panel>
          <Filter.ActiveFilters />
        </Filter.Root>
      </List.Item.Basic>

      <Filter.NoResults />

      {filtered.map((item) => (
        <List.Item.Basic key={item.id} title={item.name} />
      ))}
    </List.Container>
  )
}
```

## Decoupled hook usage

`Filter.useFilter(id)` can be called from a completely separate component tree. The filter UI and the data consumer are linked only by the shared `id`:

```tsx
function FilterUI() {
  return (
    <Filter.Root id="transactions">
      <Filter.Toolbar>
        <Filter.Search label="Søk" />
        <Filter.Toolbar.Actions>
          <Filter.PanelButton />
        </Filter.Toolbar.Actions>
      </Filter.Toolbar>
      <Filter.Panel>
        <Filter.Selection
          label="Type"
          filterKey="/type"
          data={[
            { value: 'card', label: 'Kort' },
            { value: 'transfer', label: 'Overføring' },
          ]}
        />
      </Filter.Panel>
      <Filter.ActiveFilters />
      <Filter.NoResults />
    </Filter.Root>
  )
}

function TransactionList() {
  const { search, filters, hasActiveFilters } =
    Filter.useFilter('transactions')

  // Use search/filters to filter your data
}
```

## Custom filters

Create custom filter types using `Filter.useFilterContext()` and `Filter.Item`:

```tsx
function AmountRangeFilter({ label, filterKey }) {
  const { setFilter, getFilter } = Filter.useFilterContext()
  const current = getFilter(filterKey)

  const handleChange = (min, max) => {
    if (min == null && max == null) {
      setFilter(filterKey, undefined)
    } else {
      setFilter(filterKey, {
        value: { min, max },
        label: `${label}: ${min ?? ''}–${max ?? ''}`,
      })
    }
  }

  return (
    <Filter.Item label={label} filterKey={filterKey}>
      <Flex.Horizontal>
        <Input
          label="Min"
          onChange={({ value }) =>
            handleChange(value, current?.value?.max)
          }
        />
        <Input
          label="Max"
          onChange={({ value }) =>
            handleChange(current?.value?.min, value)
          }
        />
      </Flex.Horizontal>
    </Filter.Item>
  )
}

// Usage inside Filter.Panel:
render(
  <Filter.Panel>
    <AmountRangeFilter label="Beløp" filterKey="/amount" />
  </Filter.Panel>
)
```

## Async data fetching

`Filter.useFilterAsync(id, fetcher)` handles the full fetch lifecycle — loading state, race conditions, and result count — so you don't have to wire it up yourself.

It calls your `fetcher` whenever the linked filter state changes and syncs `resultLoading` and `resultCount` to the shared state. That means `Filter.Content` and `Filter.NoResults` react automatically.

```tsx
function TransactionList() {
  const { data } = Filter.useFilterAsync(
    'my-filter',
    async ({ filters, search }) => {
      const res = await fetch(
        `/api/transactions?q=${encodeURIComponent(search)}`
      )
      return res.json()
    },
    { initialData: [] }
  )

  return (
    <Filter.Content connectedTo="my-filter">
      {data.map((tx) => (
        <p key={tx.id}>{tx.name}</p>
      ))}
    </Filter.Content>
  )
}
```

The hook returns `{ data, loading, error }`. If the fetcher returns an array, `resultCount` is set to its length automatically. Pass `initialData` to render immediately before the first fetch resolves.

Use the `debounce` option (in milliseconds) to delay the fetcher while the user is still typing. The initial fetch always runs immediately.

```tsx
const { data } = Filter.useFilterAsync('my-filter', fetcher, {
  initialData: [],
  debounce: 300,
})
```

## Accessibility

The Filter component includes several accessibility features out of the box:

### Live announcements

`Filter.Content` uses an `aria-live` region to announce filter result changes to screen readers:

- When the result count changes, it announces the number of results (e.g. "3 treff").
- When no results are found (`resultCount={0}`), it announces the no-results message.
- During loading, announcements are suppressed to avoid noisy updates.

### Focus management

The bottom "Hide filter" button appears when the filter options and the distance from the panel toggle exceed the viewport height. It updates when categories expand or collapse, content changes size, or the viewport resizes. Scrolling alone does not change its visibility, but mobile browser toolbars may resize the viewport while scrolling and trigger an update. Short panels can be closed with `Filter.PanelButton`. If no panel toggle is rendered, the bottom close button remains available. Manual mode always shows the Apply and Cancel buttons.

When the filter panel is closed with the bottom "Hide filter" button, the "Apply" button, or the "Cancel" button in manual mode, focus returns to `Filter.PanelButton`. This ensures keyboard users don't lose their place in the page.

### ARIA attributes

- `Filter.Root` renders with `role="search"` and an `aria-label` to identify the filter region.
- `Filter.PanelButton` uses `aria-expanded` to communicate whether the panel is open or closed.
- `Filter.ActiveFilters` renders a labeled group so screen readers can identify the active filter tags.

### Find-in-page

When `openOnFind` is enabled, the panel and collapsible active filters keep closed content findable by the browser's find-in-page feature, powered by [HeightAnimation](/web/components/height-animation/#accessibility); see its accessibility notes for behavior details and browser support.

## Relevant links

- [Figma](https://www.figma.com/design/cdtwQD8IJ7pTeE45U148r1/%F0%9F%92%BB-Eufemia---Web?node-id=15807-0)
- [Source code](https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-eufemia/src/components/filter)
- [Docs code](https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-design-system-portal/src/docs/web/components/filter)


## Related components

Filter is part of the [Inputs](/web/components/overview/#input) category. More components in this category:

- [Autocomplete](/web/components/autocomplete/) – to help people find and choose from matching suggestions as they type.
- [Checkbox](/web/components/checkbox/) – when people can turn one or more options on or off.
- [Date picker](/web/components/date-picker/) – when people need to choose one date or a date range.
- [Dropdown](/web/components/dropdown/) – when people need to choose one option from a list.
- [Input](/web/components/input/) – when people need to enter a short line of text.
- [Masked input](/web/components/input-masked/) – when people must enter text in a fixed format.

[See all in Inputs](/web/components/overview/#input)


## Demos

### Basic usage \{#basic-usage-demo\}

Combines `Filter.Date` and `Filter.Selection` inside `Filter.Panel`, with search, toolbar tools, and `resultCount` for the number of matching transactions. Uses the list layout pattern.


```tsx
const Example = () => {
  const transactions = [{
    id: 1,
    name: 'Rema 1000',
    amount: -245,
    type: 'card'
  }, {
    id: 2,
    name: 'DNB Salary',
    amount: 25000,
    type: 'transfer'
  }, {
    id: 3,
    name: 'Elkjøp',
    amount: -3999,
    type: 'card'
  }];
  const {
    filters,
    search
  } = Filter.useFilter('date-selection-demo');
  const filtered = transactions.filter(tx => {
    if (search && !tx.name.toLowerCase().includes(search.toLowerCase()) && !String(tx.amount).includes(search)) {
      return false;
    }
    const selectedTypes = Object.keys(filters).filter(key => key.startsWith('/type/')).map(key => key.replace('/type/', ''));
    if (selectedTypes.length > 0 && !selectedTypes.includes(tx.type)) {
      return false;
    }
    return true;
  });
  return <>
              <Filter.Root id="date-selection-demo" aria-label="Filter transactions by date and payment type" resultCount={filtered.length}>
                <Filter.Header>
                  <Filter.Toolbar>
                    <Filter.Search label="Label" placeholder="Store name, amount..." />
                    <Filter.Toolbar.Actions>
                      <Button variant="tertiary" icon={downloadIcon} iconPosition="left">
                        Download
                      </Button>
                      <Filter.PanelButton />
                    </Filter.Toolbar.Actions>
                  </Filter.Toolbar>

                  <Filter.Panel>
                    <Filter.Date />
                    <Filter.Selection label="Payment type" filterKey="/type" data={[{
            value: 'card',
            label: 'Card'
          }, {
            value: 'transfer',
            label: 'Transfer'
          }]} />
                  </Filter.Panel>

                  <Filter.ActiveFilters />
                  <Filter.ResultCount />
                </Filter.Header>
              </Filter.Root>

              <Filter.Content connectedTo="date-selection-demo">
                <List.Container>
                  <Filter.NoResults />
                  {filtered.map(tx => <List.Item.Basic key={tx.id} title={<Filter.Highlighting>
                          {tx.name}
                        </Filter.Highlighting>}>
                      <List.Cell.End>
                        <Value.Currency value={tx.amount} />
                      </List.Cell.End>
                    </List.Item.Basic>)}
                </List.Container>
              </Filter.Content>
            </>;
};
render(<Example />);
```


### Custom filter type

Build your own filter using `Filter.useFilterContext()` and `Filter.Item`. This example shows a toggle filter alongside the built-in `Filter.Selection`.


```tsx
function ToggleFilter({
  label,
  filterKey
}) {
  const {
    setFilter,
    getFilter
  } = Filter.useFilterContext();
  const isActive = !!getFilter(filterKey);
  return <Filter.Item label={label} filterKey={filterKey}>
              <ToggleButton checked={isActive} onChange={({
      checked
    }) => {
      if (checked) {
        setFilter(filterKey, {
          value: true,
          label
        });
      } else {
        setFilter(filterKey, undefined);
      }
    }}>
                {label}
              </ToggleButton>
            </Filter.Item>;
}
const Example = () => {
  const places = [{
    id: 1,
    name: 'Olivia Restaurant',
    category: 'restaurant',
    favorite: true
  }, {
    id: 2,
    name: 'Grand Hotel',
    category: 'hotel',
    favorite: false
  }, {
    id: 3,
    name: 'Kaffebrenneriet',
    category: 'cafe',
    favorite: true
  }, {
    id: 4,
    name: 'Maaemo',
    category: 'restaurant',
    favorite: false
  }];
  const {
    filters,
    search
  } = Filter.useFilter('custom-demo');
  const selectedCategories = Object.keys(filters).filter(key => key.startsWith('/category/')).map(key => key.replace('/category/', ''));
  const favoritesOnly = !!filters['/favorites'];
  const filtered = places.filter(place => {
    if (search && !place.name.toLowerCase().includes(search.toLowerCase())) {
      return false;
    }
    if (selectedCategories.length > 0 && !selectedCategories.includes(place.category)) {
      return false;
    }
    if (favoritesOnly && !place.favorite) {
      return false;
    }
    return true;
  });
  return <>
              <Filter.Root id="custom-demo" aria-label="Filter transactions by category" resultCount={filtered.length}>
                <Filter.Header>
                  <Filter.Toolbar>
                    <Filter.Search label="Search" placeholder="Search..." />
                    <Filter.Toolbar.Actions>
                      <Filter.PanelButton />
                    </Filter.Toolbar.Actions>
                  </Filter.Toolbar>
                  <Filter.Panel>
                    <Filter.Selection label="Category" filterKey="/category" data={[{
            value: 'restaurant',
            label: 'Restaurant'
          }, {
            value: 'hotel',
            label: 'Hotel'
          }, {
            value: 'cafe',
            label: 'Cafe'
          }]} />
                    <ToggleFilter label="Favorites only" filterKey="/favorites" />
                  </Filter.Panel>
                  <Filter.ActiveFilters />
                  <Filter.ResultCount />
                </Filter.Header>
              </Filter.Root>

              <Filter.Content connectedTo="custom-demo">
                <List.Container>
                  <Filter.NoResults />
                  {filtered.map(place => <List.Item.Basic key={place.id} title={<Filter.Highlighting>
                          {place.name}
                        </Filter.Highlighting>} />)}
                </List.Container>
              </Filter.Content>
            </>;
};
render(<Example />);
```


### Async result count

When the result count comes from an API, use `resultLoading` to show a loading state while the request is in progress. Open the filter panel and change a filter to see the skeleton effect. This example uses `debounce: 300` to delay the API call while the user is typing.


```tsx
const allTransactions = [{
  id: 1,
  name: 'Rema 1000',
  amount: -245,
  status: 'active'
}, {
  id: 2,
  name: 'DNB Salary',
  amount: 25000,
  status: 'active'
}, {
  id: 3,
  name: 'Elkjøp',
  amount: -3999,
  status: 'inactive'
}, {
  id: 4,
  name: 'Kiwi',
  amount: -189,
  status: 'active'
}, {
  id: 5,
  name: 'Spotify',
  amount: -119,
  status: 'inactive'
}];

// Simulates an API call with a delay
// Simulates an API call with a delay
function fetchFiltered(filters, search) {
  return new Promise<typeof allTransactions>(resolve => {
    setTimeout(() => {
      const result = allTransactions.filter(tx => {
        if (search && !tx.name.toLowerCase().includes(search.toLowerCase()) && !String(tx.amount).includes(search)) {
          return false;
        }
        const selectedStatuses = Object.keys(filters).filter(key => key.startsWith('/status/')).map(key => key.replace('/status/', ''));
        if (selectedStatuses.length > 0 && !selectedStatuses.includes(tx.status)) {
          return false;
        }
        return true;
      });
      resolve(result);
    }, 1000);
  });
}
const Example = () => {
  const {
    data: filtered
  } = Filter.useFilterAsync('async-demo', ({
    filters,
    search
  }) => fetchFiltered(filters, search), {
    initialData: allTransactions,
    debounce: 300
  });
  return <>
              <Filter.Root id="async-demo" aria-label="Filter orders loaded asynchronously">
                <Filter.Header>
                  <Filter.Toolbar>
                    <Filter.Search label="Search" placeholder="Search for something..." />
                    <Filter.Toolbar.Actions>
                      <Filter.Date />
                      <Filter.PanelButton />
                    </Filter.Toolbar.Actions>
                  </Filter.Toolbar>
                  <Filter.Panel>
                    <Filter.Selection label="Status" filterKey="/status" defaultOpen data={[{
            value: 'active',
            label: 'Active'
          }, {
            value: 'inactive',
            label: 'Inactive'
          }]} />
                  </Filter.Panel>
                  <Filter.ActiveFilters />
                  <Filter.ResultCount />
                </Filter.Header>
              </Filter.Root>

              <Filter.Content connectedTo="async-demo">
                <List.Container>
                  <Filter.NoResults />
                  {filtered.map(tx => <List.Item.Basic key={tx.id} title={<Filter.Highlighting>
                          {tx.name}
                        </Filter.Highlighting>}>
                      <List.Cell.End>
                        <Value.Currency value={tx.amount} />
                      </List.Cell.End>
                    </List.Item.Basic>)}
                </List.Container>
              </Filter.Content>
            </>;
};
render(<Example />);
```


### Manual behavior

With `behavior="manual"`, panel filter changes are buffered internally and not emitted until the user clicks "Apply filter". Search input is still emitted in real time. The panel shows an Apply button and a Cancel button that reverts unsaved changes.


```tsx
const allTransactions = [{
  id: 1,
  name: 'Rema 1000',
  amount: -245,
  status: 'active'
}, {
  id: 2,
  name: 'DNB Salary',
  amount: 25000,
  status: 'active'
}, {
  id: 3,
  name: 'Elkjøp',
  amount: -3999,
  status: 'inactive'
}, {
  id: 4,
  name: 'Kiwi',
  amount: -189,
  status: 'active'
}, {
  id: 5,
  name: 'Spotify',
  amount: -119,
  status: 'inactive'
}];
function fetchFiltered(filters, search) {
  return new Promise<typeof allTransactions>(resolve => {
    setTimeout(() => {
      const result = allTransactions.filter(tx => {
        if (search && !tx.name.toLowerCase().includes(search.toLowerCase()) && !String(tx.amount).includes(search)) {
          return false;
        }
        const selectedStatuses = Object.keys(filters).filter(key => key.startsWith('/status/')).map(key => key.replace('/status/', ''));
        if (selectedStatuses.length > 0 && !selectedStatuses.includes(tx.status)) {
          return false;
        }
        return true;
      });
      resolve(result);
    }, 1000);
  });
}
const Example = () => {
  const {
    data: filtered
  } = Filter.useFilterAsync('manual-demo', ({
    filters,
    search
  }) => fetchFiltered(filters, search), {
    initialData: allTransactions
  });
  return <>
              <Filter.Root id="manual-demo" aria-label="Filter transactions with Apply" behavior="manual">
                <Filter.Header>
                  <Filter.Toolbar>
                    <Filter.Search label="Search" placeholder="Search for something..." />
                    <Filter.Toolbar.Actions>
                      <Filter.PanelButton />
                    </Filter.Toolbar.Actions>
                  </Filter.Toolbar>
                  <Filter.Panel>
                    <Filter.Selection label="Status" filterKey="/status" defaultOpen data={[{
            value: 'active',
            label: 'Active'
          }, {
            value: 'inactive',
            label: 'Inactive'
          }]} />
                  </Filter.Panel>
                  <Filter.ActiveFilters />
                  <Filter.ResultCount />
                </Filter.Header>
              </Filter.Root>

              <Filter.Content connectedTo="manual-demo">
                <List.Container>
                  <Filter.NoResults />
                  {filtered.map(tx => <List.Item.Basic key={tx.id} title={<Filter.Highlighting>
                          {tx.name}
                        </Filter.Highlighting>}>
                      <List.Cell.End>
                        <Value.Currency value={tx.amount} />
                      </List.Cell.End>
                    </List.Item.Basic>)}
                </List.Container>
              </Filter.Content>
            </>;
};
render(<Example />);
```


### Predefined filters

Use `defaultFilters` to pre-select filters on mount. The panel and relevant filter accordions open automatically.


```tsx
const Example = () => {
  const transactions = [{
    id: 1,
    name: 'Rema 1000',
    amount: -245,
    type: 'card'
  }, {
    id: 2,
    name: 'DNB Salary',
    amount: 25000,
    type: 'transfer'
  }, {
    id: 3,
    name: 'Elkjøp',
    amount: -3999,
    type: 'card'
  }];
  const {
    filters,
    search
  } = Filter.useFilter('predefined-demo');
  const filtered = transactions.filter(tx => {
    if (search && !tx.name.toLowerCase().includes(search.toLowerCase()) && !String(tx.amount).includes(search)) {
      return false;
    }
    const selectedTypes = Object.keys(filters).filter(key => key.startsWith('/type/')).map(key => key.replace('/type/', ''));
    if (selectedTypes.length > 0 && !selectedTypes.includes(tx.type)) {
      return false;
    }
    return true;
  });
  return <>
              <Filter.Root id="predefined-demo" aria-label="Filter transactions with predefined filters" resultCount={filtered.length} defaultFilters={{
      '/type/card': {
        value: 'card',
        label: 'Card',
        categoryLabel: 'Payment type'
      },
      '/type/transfer': {
        value: 'transfer',
        label: 'Transfer',
        categoryLabel: 'Payment type'
      },
      '/status/pending': {
        value: 'pending',
        label: 'Pending',
        categoryLabel: 'Status'
      },
      '/status/completed': {
        value: 'completed',
        label: 'Completed',
        categoryLabel: 'Status'
      },
      '/status/failed': {
        value: 'failed',
        label: 'Failed',
        categoryLabel: 'Status'
      },
      '/region/oslo': {
        value: 'oslo',
        label: 'Oslo',
        categoryLabel: 'Region'
      },
      '/region/bergen': {
        value: 'bergen',
        label: 'Bergen',
        categoryLabel: 'Region'
      },
      '/region/trondheim': {
        value: 'trondheim',
        label: 'Trondheim',
        categoryLabel: 'Region'
      },
      '/category/groceries': {
        value: 'groceries',
        label: 'Groceries',
        categoryLabel: 'Category'
      },
      '/category/electronics': {
        value: 'electronics',
        label: 'Electronics',
        categoryLabel: 'Category'
      },
      '/category/salary': {
        value: 'salary',
        label: 'Salary',
        categoryLabel: 'Category'
      },
      '/category/subscription': {
        value: 'subscription',
        label: 'Subscription',
        categoryLabel: 'Category'
      }
    }}>
                <Filter.Header>
                  <Filter.Toolbar>
                    <Filter.Search label="Label" placeholder="Store name, amount..." />
                    <Filter.Toolbar.Actions>
                      <Button variant="tertiary" icon={downloadIcon} iconPosition="left">
                        Download
                      </Button>
                      <Filter.PanelButton />
                    </Filter.Toolbar.Actions>
                  </Filter.Toolbar>

                  <Filter.Panel>
                    <Filter.Date />
                    <Filter.Selection label="Payment type" filterKey="/type" data={[{
            value: 'card',
            label: 'Card'
          }, {
            value: 'transfer',
            label: 'Transfer'
          }]} />
                  </Filter.Panel>

                  <Filter.ActiveFilters showCategoryLabel collapsibleThreshold={5} />
                  <Filter.ResultCount />
                </Filter.Header>
              </Filter.Root>

              <Filter.Content connectedTo="predefined-demo">
                <List.Container>
                  <Filter.NoResults />
                  {filtered.map(tx => <List.Item.Basic key={tx.id} title={<Filter.Highlighting>
                          {tx.name}
                        </Filter.Highlighting>}>
                      <List.Cell.End>
                        <Value.Currency value={tx.amount} />
                      </List.Cell.End>
                    </List.Item.Basic>)}
                </List.Container>
              </Filter.Content>
            </>;
};
render(<Example />);
```


### URL sync with router hooks

Three hooks sync filter state with URL query parameters so users can share or bookmark filtered views. Back/forward navigation restores the previous filter state.

- **`Filter.useQueryLocator(id, options?)`** — Uses the History API directly. Works without any router dependency. Pass `{ excludeSearch: true }` to exclude the search string from URL sync.
- **`Filter.useReactRouter(id, { useSearchParams, excludeSearch? })`** — Uses React Router's `useSearchParams`.
- **`Filter.useNextRouter(id, { useRouter, usePathname, useSearchParams, excludeSearch? })`** — Uses Next.js navigation hooks.


```tsx
const transactions = [{
  id: 1,
  name: 'Rema 1000',
  amount: -245,
  status: 'active'
}, {
  id: 2,
  name: 'DNB Salary',
  amount: 25000,
  status: 'active'
}, {
  id: 3,
  name: 'Elkjøp',
  amount: -3999,
  status: 'inactive'
}, {
  id: 4,
  name: 'Kiwi',
  amount: -189,
  status: 'active'
}];
const Example = () => {
  // Syncs filter state to/from URL query parameters
  Filter.useQueryLocator('query-locator-demo', {
    // excludeSearch: true, // You can exclude search from the URL if you want, by default it is included
  });
  const {
    filters,
    search
  } = Filter.useFilter('query-locator-demo');
  const selectedStatuses = Object.keys(filters).filter(key => key.startsWith('/status/')).map(key => key.replace('/status/', ''));
  const filtered = transactions.filter(tx => {
    if (search && !tx.name.toLowerCase().includes(search.toLowerCase()) && !String(tx.amount).includes(search)) {
      return false;
    }
    if (selectedStatuses.length > 0 && !selectedStatuses.includes(tx.status)) {
      return false;
    }
    return true;
  });
  return <>
              <Filter.Root id="query-locator-demo" aria-label="Filter transactions with URL state" resultCount={filtered.length}>
                <Filter.Header>
                  <Filter.Toolbar>
                    <Filter.Search label="Search" placeholder="Store name..." />
                    <Filter.Toolbar.Actions>
                      <Filter.PanelButton />
                    </Filter.Toolbar.Actions>
                  </Filter.Toolbar>
                  <Filter.Panel>
                    <Filter.Selection label="Status" filterKey="/status" data={[{
            value: 'active',
            label: 'Active'
          }, {
            value: 'inactive',
            label: 'Inactive'
          }]} />
                  </Filter.Panel>
                  <Filter.ActiveFilters />
                  <Filter.ResultCount />
                </Filter.Header>
              </Filter.Root>

              <Filter.Content connectedTo="query-locator-demo">
                <List.Container>
                  <Filter.NoResults />
                  {filtered.map(tx => <List.Item.Basic key={tx.id} title={<Filter.Highlighting>
                          {tx.name}
                        </Filter.Highlighting>}>
                      <List.Cell.End>
                        <Value.Currency value={tx.amount} />
                      </List.Cell.End>
                    </List.Item.Basic>)}
                </List.Container>
              </Filter.Content>
            </>;
};
render(<Example />);
```


### With sort button

Use `Filter.SortButton` to add a sort dropdown to the toolbar. It renders a tertiary Dropdown with a sort icon and independent width. The sort state is managed outside the filter.


```tsx
const Example = () => {
  const transactions = [{
    id: 1,
    name: 'Rema 1000',
    amount: -245
  }, {
    id: 2,
    name: 'DNB Salary',
    amount: 25000
  }, {
    id: 3,
    name: 'Elkjøp',
    amount: -3999
  }, {
    id: 4,
    name: 'Kiwi',
    amount: -189
  }];
  const sortOptions = [{
    selectedKey: 'newest',
    content: 'Newest first'
  }, {
    selectedKey: 'oldest',
    content: 'Oldest first'
  }, {
    selectedKey: 'amount-high',
    content: 'Amount high–low'
  }, {
    selectedKey: 'amount-low',
    content: 'Amount low–high'
  }];
  const [sortKey, setSortKey] = useState('newest');
  const {
    search
  } = Filter.useFilter('sort-demo');
  const filtered = transactions.filter(tx => {
    if (search && !tx.name.toLowerCase().includes(search.toLowerCase()) && !String(tx.amount).includes(search)) {
      return false;
    }
    return true;
  }).sort((a, b) => {
    switch (sortKey) {
      case 'oldest':
        return a.id - b.id;
      case 'amount-high':
        return b.amount - a.amount;
      case 'amount-low':
        return a.amount - b.amount;
      default:
        return b.id - a.id;
    }
  });
  return <>
              <Filter.Root id="sort-demo" aria-label="Filter and sort transactions" resultCount={filtered.length}>
                <Filter.Header>
                  <Filter.Toolbar>
                    <Filter.Search label="Search" placeholder="Store name..." />
                    <Filter.Toolbar.Actions>
                      <Filter.SortButton data={sortOptions} value={sortKey} onChange={({
              data: {
                selectedKey
              }
            }) => {
              setSortKey(String(selectedKey));
            }} />
                    </Filter.Toolbar.Actions>
                  </Filter.Toolbar>
                  <Filter.ResultCount />
                </Filter.Header>
              </Filter.Root>

              <Filter.Content connectedTo="sort-demo">
                <List.Container>
                  <Filter.NoResults />
                  {filtered.map(tx => <List.Item.Basic key={tx.id} title={<Filter.Highlighting>
                          {tx.name}
                        </Filter.Highlighting>}>
                      <List.Cell.End>
                        <Value.Currency value={tx.amount} />
                      </List.Cell.End>
                    </List.Item.Basic>)}
                </List.Container>
              </Filter.Content>
            </>;
};
render(<Example />);
```


### Quick filters

Quick filters are toggle buttons placed directly below the toolbar, outside the panel. They let users apply common filters without opening the panel.


```tsx
function QuickFilter({
  label,
  filterKey
}) {
  const {
    setFilter,
    getFilter
  } = Filter.useFilterContext();
  const isActive = !!getFilter(filterKey);
  return <ToggleButton checked={isActive} onChange={({
    checked
  }) => {
    if (checked) {
      setFilter(filterKey, {
        value: true,
        label
      });
    } else {
      setFilter(filterKey, undefined);
    }
  }}>
              {label}
            </ToggleButton>;
}
const Example = () => {
  const transactions = [{
    id: 1,
    name: 'Rema 1000',
    amount: -245,
    type: 'card'
  }, {
    id: 2,
    name: 'DNB Salary',
    amount: 25000,
    type: 'transfer'
  }, {
    id: 3,
    name: 'Elkjøp',
    amount: -3999,
    type: 'card'
  }, {
    id: 4,
    name: 'Kiwi',
    amount: -189,
    type: 'card'
  }];
  const {
    filters,
    search
  } = Filter.useFilter('quick-filters-demo');
  const showCards = !!filters['/card'];
  const showTransfers = !!filters['/transfer'];
  const filtered = transactions.filter(tx => {
    if (search && !tx.name.toLowerCase().includes(search.toLowerCase()) && !String(tx.amount).includes(search)) {
      return false;
    }
    if (showCards && tx.type !== 'card') {
      return false;
    }
    if (showTransfers && tx.type !== 'transfer') {
      return false;
    }
    return true;
  });
  return <>
              <Filter.Root id="quick-filters-demo" aria-label="Filter transactions with quick filters" resultCount={filtered.length}>
                <Filter.Header>
                  <Filter.QuickFilters>
                    <QuickFilter label="Card" filterKey="/card" />
                    <QuickFilter label="Transfer" filterKey="/transfer" />
                  </Filter.QuickFilters>
                  <Filter.ResultCount />
                </Filter.Header>
              </Filter.Root>

              <Filter.Content connectedTo="quick-filters-demo">
                <List.Container>
                  <Filter.NoResults />
                  {filtered.map(tx => <List.Item.Action key={tx.id} title={<Filter.Highlighting>
                          {tx.name}
                        </Filter.Highlighting>}>
                      <List.Cell.End>
                        <Value.Currency value={tx.amount} />
                      </List.Cell.End>
                    </List.Item.Action>)}
                </List.Container>
              </Filter.Content>
            </>;
};
render(<Example />);
```


### Toolbar with actions only

A toolbar with only action buttons and no search field.


```tsx
const Example = () => {
  const items = [{
    id: 1,
    name: 'Report Q1',
    amount: 12000
  }, {
    id: 2,
    name: 'Report Q2',
    amount: 15000
  }, {
    id: 3,
    name: 'Report Q3',
    amount: 9800
  }];
  return <>
              <Filter.Root id="actions-only-demo" aria-label="Filter toolbar actions">
                <Filter.Header>
                  <Filter.Toolbar>
                    <Filter.Toolbar.Actions>
                      <Button variant="tertiary" icon={tableIcon} iconPosition="left">
                        Layout
                      </Button>
                      <Button variant="tertiary" icon={downloadIcon} iconPosition="left">
                        Download
                      </Button>
                    </Filter.Toolbar.Actions>
                  </Filter.Toolbar>
                </Filter.Header>
              </Filter.Root>

              <Filter.Content connectedTo="actions-only-demo">
                <List.Container>
                  {items.map(item => <List.Item.Action key={item.id} title={<Filter.Highlighting>
                          {item.name}
                        </Filter.Highlighting>}>
                      <List.Cell.End>
                        <Value.Currency value={item.amount} />
                      </List.Cell.End>
                    </List.Item.Action>)}
                </List.Container>
              </Filter.Content>
            </>;
};
render(<Example />);
```


### Search only

A simple search field with a secondary search button.


```tsx
const Example = () => {
  const items = useMemo(() => [{
    id: 1,
    name: 'Rema 1000',
    amount: -245
  }, {
    id: 2,
    name: 'Kiwi',
    amount: -189
  }, {
    id: 3,
    name: 'Salary',
    amount: 35000
  }], []);
  const getFilteredItems = useCallback((searchValue: string) => {
    return items.filter(item => {
      if (searchValue && !item.name.toLowerCase().includes(searchValue.toLowerCase()) && !String(item.amount).includes(searchValue)) {
        return false;
      }
      return true;
    });
  }, [items]);
  const {
    search
  } = Filter.useFilter('search-only-demo');
  const previousSearchRef = useRef(search);
  const [showSkeleton, setShowSkeleton] = useState(false);
  const [filtered, setFiltered] = useState(() => getFilteredItems(search));
  useEffect(() => {
    if (previousSearchRef.current === search) {
      return; // stop here
    }
    previousSearchRef.current = search;
    setShowSkeleton(true);
    const timeout = setTimeout(() => {
      setFiltered(getFilteredItems(search));
      setShowSkeleton(false);
    }, 1000);
    return () => clearTimeout(timeout);
  }, [getFilteredItems, search]);
  const visibleItems = showSkeleton ? items : filtered;
  return <>
              <Filter.Root id="search-only-demo" aria-label="Search transactions" resultCount={filtered.length}>
                <Filter.Header>
                  <Filter.Toolbar>
                    <Filter.Search submitBehavior="manual" label="Search" placeholder="Search..." />
                  </Filter.Toolbar>
                  <Filter.ResultCount />
                </Filter.Header>
              </Filter.Root>

              <Filter.Content connectedTo="search-only-demo">
                <List.Container skeleton={showSkeleton}>
                  {!showSkeleton && <Filter.NoResults />}
                  {visibleItems.map(item => <List.Item.Action key={item.id} title={<Filter.Highlighting>
                          {item.name}
                        </Filter.Highlighting>}>
                      <List.Cell.End>
                        <Value.Currency value={item.amount} />
                      </List.Cell.End>
                    </List.Item.Action>)}
                </List.Container>
              </Filter.Content>
            </>;
};
render(<Example />);
```


### Multi-selection filter with grid layout

Use `Filter.MultiSelection` inside `Filter.Panel` to let users select one or more clients. This example uses a `Grid` layout to place the filter and results side by side.


```tsx
const clients = [{
  value: 'acme',
  title: 'Acme Corp'
}, {
  value: 'globex',
  title: 'Globex Inc'
}, {
  value: 'initech',
  title: 'Initech Ltd'
}, {
  value: 'umbrella',
  title: 'Umbrella Group'
}];
const transactions = [{
  id: 1,
  name: 'Invoice #1012',
  amount: 45000,
  client: 'acme'
}, {
  id: 2,
  name: 'Invoice #1013',
  amount: 12500,
  client: 'globex'
}, {
  id: 3,
  name: 'Credit note #204',
  amount: -3200,
  client: 'acme'
}, {
  id: 4,
  name: 'Invoice #1014',
  amount: 78000,
  client: 'initech'
}, {
  id: 5,
  name: 'Invoice #1015',
  amount: 9400,
  client: 'umbrella'
}, {
  id: 6,
  name: 'Invoice #1016',
  amount: 23000,
  client: 'globex'
}];
const Example = () => {
  const {
    filters,
    search
  } = Filter.useFilter('multi-selection-demo');
  const selectedClients = Object.keys(filters).filter(key => key.startsWith('/client/')).map(key => key.replace('/client/', ''));
  const filtered = transactions.filter(tx => {
    if (search && !tx.name.toLowerCase().includes(search.toLowerCase()) && !String(tx.amount).includes(search)) {
      return false;
    }
    if (selectedClients.length > 0 && !selectedClients.includes(tx.client)) {
      return false;
    }
    return true;
  });
  return <Grid.Container columnGap="large" rowGap="large" style={{
    marginInline: 'auto',
    maxInlineSize: 'var(--layout-medium)'
  }}>
              <Grid.Item span={{
      small: 'full',
      large: [1, 4]
    }}>
                <Heading size="large" top={false}>
                  Filter
                </Heading>
                <Filter.Root id="multi-selection-demo" aria-label="Filter invoices by client" resultCount={filtered.length}>
                  <Filter.Toolbar>
                    <Filter.Search label="Search" placeholder="Invoice number..." />
                    <Filter.Toolbar.Actions>
                      <Filter.Date />
                      <Filter.PanelButton />
                    </Filter.Toolbar.Actions>
                  </Filter.Toolbar>
                  <Filter.Panel>
                    <Filter.MultiSelection label="Client" filterKey="/client" data={clients} />
                  </Filter.Panel>
                  <Filter.ActiveFilters />
                  <Filter.ResultCount />
                </Filter.Root>
              </Grid.Item>

              <Grid.Item span={{
      small: 'full',
      large: [5, 12]
    }}>
                <Filter.Content connectedTo="multi-selection-demo">
                  <Heading size="large" top={false}>
                    Transactions
                  </Heading>
                  <List.Container>
                    <Filter.NoResults />
                    {filtered.map(tx => <List.Item.Basic key={tx.id} title={<Filter.Highlighting>
                            {tx.name}
                          </Filter.Highlighting>}>
                        <List.Cell.End>
                          <Value.Currency value={tx.amount} />
                        </List.Cell.End>
                      </List.Item.Basic>)}
                  </List.Container>
                </Filter.Content>
              </Grid.Item>
            </Grid.Container>;
};
render(<Example />);
```


### Decoupled hook

`Filter.useFilter(id)` can be called anywhere in the tree — the filter UI and data consumer can live in completely separate components.


```tsx
function TransactionList() {
  const {
    search
  } = Filter.useFilter('decoupled-demo');
  const data = [{
    id: 1,
    name: 'Rema 1000',
    amount: -245
  }, {
    id: 2,
    name: 'Kiwi',
    amount: -189
  }, {
    id: 3,
    name: 'Salary',
    amount: 35000
  }];
  const filtered = data.filter(item => {
    if (search && !item.name.toLowerCase().includes(search.toLowerCase()) && !String(item.amount).includes(search)) {
      return false;
    }
    return true;
  });
  return <Filter.Content connectedTo="decoupled-demo">
              <P space>
                {filtered.length > 0 && <P>Antall: {filtered.length}</P>}
              </P>
              {filtered.length > 0 && <List.Container>
                  <Filter.NoResults />
                  {filtered.map(item => <List.Item.Basic key={item.id} title={<Filter.Highlighting>
                          {item.name}
                        </Filter.Highlighting>}>
                      <List.Cell.End>
                        <Value.Currency value={item.amount} />
                      </List.Cell.End>
                    </List.Item.Basic>)}
                </List.Container>}
            </Filter.Content>;
}
render(<>
            <Filter.Root id="decoupled-demo" aria-label="Filter the separate results list">
              <Filter.Search label="Search" placeholder="Search results..." />
              <Filter.ActiveFilters />
              <Filter.NoResults />
            </Filter.Root>

            <TransactionList />
          </>);
```

<!-- eufemia-properties-usage:start -->
Some properties provide flexibility for specific integrations. Follow the component's usage guidance. Customizations beyond the documented patterns need a use case approved by DNB UX. [Read about intended use and customization](/web/styling-and-themes/configuration/component-properties/#intended-use-and-customization).
<!-- eufemia-properties-usage:end -->

## Filter.Root


```json
{
  "props": {
    "aria-label": {
      "doc": "Accessible name for the search landmark. Give each Filter.Root a descriptive name when a page contains multiple search landmarks. Defaults to the translated Filter label.",
      "type": "string",
      "status": "optional"
    },
    "id": {
      "doc": "Unique identifier that links the filter UI to `Filter.useFilter(id)` consumers via shared state.",
      "type": "string",
      "status": "required"
    },
    "behavior": {
      "doc": "Controls when filter changes are emitted. `\"realtime\"` (default) emits on every change. `\"manual\"` buffers filter changes internally until the user clicks Apply in the panel. Search input is always emitted in real time.",
      "type": [
        "\"realtime\"",
        "\"manual\""
      ],
      "status": "optional"
    },
    "resultCount": {
      "doc": "The number of matching results. Passed to child components that can display the count.",
      "type": "number",
      "status": "optional"
    },
    "defaultFilters": {
      "doc": "Pre-selected filters to apply on mount. The panel and relevant filter accordions open automatically. Keys should match the filter key format (e.g. `/type/card`).",
      "type": "Record<string, FilterValue>",
      "status": "optional"
    },
    "defaultPanelOpen": {
      "doc": "Controls whether the filter panel is open on mount. When `true`, the panel opens even without active filters. When `false`, the panel stays closed even with `defaultFilters` or URL-restored filters. Defaults to opening automatically when filters are present.",
      "type": "boolean",
      "status": "optional"
    },
    "resultLoading": {
      "doc": "When `true`, the result count shows a skeleton loading state. Use this while fetching result counts asynchronously.",
      "type": "boolean",
      "status": "optional"
    },
    "className": {
      "doc": "Custom CSS class name applied to the root element.",
      "type": "string",
      "status": "optional"
    },
    "style": {
      "doc": "Inline styles applied to the root element. Use e.g. `maxWidth` to control the width of the filter.",
      "type": "CSSProperties",
      "status": "optional"
    },
    "children": {
      "doc": "Filter sub-components like `Filter.Search`, `Filter.Panel`, `Filter.ActiveFilters`, etc.",
      "type": "React.ReactNode",
      "status": "optional"
    },
    "[Space](/web/layout-and-typography/layout/space/properties)": {
      "doc": "Spacing properties like `top` or `bottom` are supported.",
      "type": [
        "string",
        "object"
      ],
      "status": "optional"
    }
  },
  "usageGuidance": "Some properties provide flexibility for specific integrations. Follow the component's usage guidance. Customizations beyond the documented patterns need a use case approved by DNB UX. [Read about intended use and customization](/web/styling-and-themes/configuration/component-properties/#intended-use-and-customization)."
}
```


## Filter.Header


```json
{
  "props": {
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    },
    "children": {
      "doc": "Filter controls like `Filter.Toolbar`, `Filter.Panel`, and `Filter.ActiveFilters`.",
      "type": "React.ReactNode",
      "status": "optional"
    }
  }
}
```


## Filter.Search


```json
{
  "props": {
    "label": {
      "doc": "Label for the search input.",
      "type": "string",
      "status": "required"
    },
    "submitBehavior": {
      "doc": "When set to `\"manual\"`, the search state is only updated when the user presses Enter or clicks the submit button. Automatically applies `type=\"search\"` and explicitly shows the submit button.",
      "type": "\"manual\"",
      "status": "optional"
    },
    "placeholder": {
      "doc": "Placeholder text for the search input.",
      "type": "string",
      "status": "optional"
    },
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    }
  }
}
```


## Filter.Panel


```json
{
  "props": {
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    },
    "openOnFind": {
      "doc": "Keeps closed panel content findable by the browser's find-in-page feature. Matching content opens the panel. Defaults to `false`.",
      "type": "boolean",
      "status": "optional"
    },
    "children": {
      "doc": "Filter components rendered inside the panel, e.g. `Filter.Selection`, `Filter.Date`, or custom filters.",
      "type": "React.ReactNode",
      "status": "optional"
    }
  }
}
```


## Filter.PanelButton


```json
{
  "props": {
    "children": {
      "doc": "Button label text. Defaults to the translated `\"Filter\"` label.",
      "type": "React.ReactNode",
      "status": "optional"
    },
    "[Button props]": {
      "doc": "All [Button](/web/components/button/properties) props are supported except `variant`, `icon`, `iconPosition`, `transitionState`, and `aria-expanded`.",
      "type": "Various",
      "status": "optional"
    }
  }
}
```


## Filter.ActiveFilters


```json
{
  "props": {
    "label": {
      "doc": "Accessible label for the active filters group. Defaults to `\"Aktive filtre\"`.",
      "type": "string",
      "status": "optional"
    },
    "showCategoryLabel": {
      "doc": "When `true`, each tag is prefixed with the category name (e.g. \"Betalingstype: Kort\" instead of \"Kort\").",
      "type": "boolean",
      "status": "optional"
    },
    "collapsibleThreshold": {
      "doc": "When the number of active filters exceeds this threshold, tags are shown inside a collapsible accordion with a scrollable area and a \"Clear all\" button.",
      "type": "number",
      "status": "optional"
    },
    "openOnFind": {
      "doc": "Keeps collapsed active filters findable by the browser's find-in-page feature. Matching content expands the active filters. Defaults to `false`.",
      "type": "boolean",
      "status": "optional"
    },
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    }
  }
}
```


## Filter.Toolbar


```json
{
  "props": {
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    },
    "children": {
      "doc": "Toolbar content — typically `Filter.Search` on the left and `Filter.Toolbar.Actions` grouping action buttons on the right.",
      "type": "React.ReactNode",
      "status": "optional"
    }
  }
}
```


## Filter.Toolbar.Actions


```json
{
  "props": {
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    },
    "children": {
      "doc": "Action buttons or controls to display on the right side of the toolbar.",
      "type": "React.ReactNode",
      "status": "optional"
    }
  }
}
```


## Filter.Item


```json
{
  "props": {
    "label": {
      "doc": "Label displayed for this filter section.",
      "type": "string",
      "status": "required"
    },
    "filterKey": {
      "doc": "Unique key identifying this filter in the state.",
      "type": "string",
      "status": "required"
    },
    "defaultOpen": {
      "doc": "When `true`, the accordion starts expanded. The open/closed state is remembered across panel opens without a page refresh.",
      "type": "boolean",
      "status": "optional"
    },
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    },
    "children": {
      "doc": "Filter content rendered inside the item.",
      "type": "React.ReactNode",
      "status": "optional"
    }
  }
}
```


## Filter.Date


```json
{
  "props": {
    "label": {
      "doc": "Label for the date filter. Defaults to the locale translation (e.g. \"Dato\" in Norwegian, \"Date\" in English).",
      "type": "string",
      "status": "optional"
    },
    "filterKey": {
      "doc": "Unique key for this filter in the state. Defaults to `\"date\"`.",
      "type": "string",
      "status": "optional"
    },
    "defaultOpen": {
      "doc": "When `true`, the accordion starts expanded. The state is remembered across panel opens.",
      "type": "boolean",
      "status": "optional"
    }
  }
}
```


## Filter.Selection


```json
{
  "props": {
    "label": {
      "doc": "Label for the selection filter.",
      "type": "string",
      "status": "required"
    },
    "filterKey": {
      "doc": "Unique key for this filter group. Individual selections are stored as `filterKey/value` entries in the filter state.",
      "type": "string",
      "status": "required"
    },
    "data": {
      "doc": "Array of selectable items. Each item has a `value` (string) and `label` (string).",
      "type": "Array<{ value: string; label: string }>",
      "status": "required"
    },
    "defaultOpen": {
      "doc": "When `true`, the accordion starts expanded. The state is remembered across panel opens.",
      "type": "boolean",
      "status": "optional"
    }
  }
}
```


## Filter.MultiSelection


```json
{
  "props": {
    "label": {
      "doc": "Label for the multi-selection filter.",
      "type": "string",
      "status": "required"
    },
    "filterKey": {
      "doc": "Unique key for this filter group. Individual selections are stored as `filterKey/value` entries in the filter state.",
      "type": "string",
      "status": "required"
    },
    "data": {
      "doc": "Array of selectable items. Each item has a `value` (string | number) and `title` (ReactNode).",
      "type": "Array<{ value: string | number; title: React.ReactNode }>",
      "status": "required"
    },
    "defaultOpen": {
      "doc": "When `true`, the accordion starts expanded. The state is remembered across panel opens.",
      "type": "boolean",
      "status": "optional"
    }
  }
}
```


## Filter.QuickFilters


```json
{
  "props": {
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    },
    "children": {
      "doc": "Quick filter toggle buttons or other controls.",
      "type": "React.ReactNode",
      "status": "optional"
    }
  }
}
```


## Filter.SortButton


```json
{
  "props": {
    "data": {
      "doc": "Sort options passed to the underlying Dropdown. Each item should have a `selectedKey` and `content`.",
      "type": "DrawerListData",
      "status": "required"
    },
    "value": {
      "doc": "The currently selected sort value.",
      "type": [
        "string",
        "number"
      ],
      "status": "optional"
    },
    "defaultValue": {
      "doc": "Default sort value on mount.",
      "type": [
        "string",
        "number"
      ],
      "status": "optional"
    },
    "size": {
      "doc": "Size of the trigger button. Defaults to `\"medium\"`.",
      "type": [
        "\"default\"",
        "\"small\"",
        "\"medium\"",
        "\"large\""
      ],
      "status": "optional"
    },
    "[Dropdown props]": {
      "doc": "All other [Dropdown](/web/components/dropdown/properties) props are forwarded.",
      "type": "Various",
      "status": "optional"
    }
  }
}
```


## Filter.NoResults


```json
{
  "props": {
    "connectedTo": {
      "doc": "Links to a `Filter.Root` by its `id`. Reads `resultCount` from the shared filter state. When used, the `resultCount` prop is not needed.",
      "type": "string",
      "status": "optional"
    },
    "resultCount": {
      "doc": "The number of results. When `0`, the no-results message is shown. When omitted, falls back to the `resultCount` from the linked `connectedTo` or the nearest `Filter.Root`.",
      "type": "number",
      "status": "optional"
    },
    "children": {
      "doc": "Custom message to display instead of the default translated text.",
      "type": "string",
      "status": "optional"
    },
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    },
    "[Space](/web/layout-and-typography/layout/space/properties)": {
      "doc": "Spacing properties like `top` or `bottom` are supported.",
      "type": [
        "string",
        "object"
      ],
      "status": "optional"
    }
  }
}
```


## Filter.ResultCount


```json
{
  "props": {
    "connectedTo": {
      "doc": "Links to a `Filter.Root` by its `id`. Reads `resultCount` from the shared filter state.",
      "type": "string",
      "status": "optional"
    },
    "resultCount": {
      "doc": "The number of results to display. When omitted, falls back to the `resultCount` from the linked `connectedTo` or nearest `Filter.Root`.",
      "type": "number",
      "status": "optional"
    },
    "alwaysVisible": {
      "doc": "When `true`, the result count is always visible even when no filters or search text are active. By default, the count is only shown when filters are active.",
      "type": "boolean",
      "status": "optional"
    },
    "children": {
      "doc": "Custom message to display instead of the default translated text.",
      "type": "string",
      "status": "optional"
    },
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    },
    "[Space](/web/layout-and-typography/layout/space/properties)": {
      "doc": "Spacing properties like `top` or `bottom` are supported.",
      "type": [
        "string",
        "object"
      ],
      "status": "optional"
    }
  }
}
```


## Filter.Content


```json
{
  "props": {
    "connectedTo": {
      "doc": "Links to a `Filter.Root` by its `id`. When omitted and used inside a `Filter.Root`, the id is inherited from context.",
      "type": "string",
      "status": "optional"
    },
    "className": {
      "doc": "Custom CSS class name.",
      "type": "string",
      "status": "optional"
    },
    "children": {
      "doc": "Result content to render. Wrapped in a skeleton when the linked filter is loading.",
      "type": "React.ReactNode",
      "status": "optional"
    },
    "[Space](/web/layout-and-typography/layout/space/properties)": {
      "doc": "Spacing properties like `top` or `bottom` are supported.",
      "type": [
        "string",
        "object"
      ],
      "status": "optional"
    }
  }
}
```


## Filter.Highlighting


```json
{
  "props": {
    "children": {
      "doc": "Text to highlight based on the current search value.",
      "type": "string",
      "status": "required"
    },
    "connectedTo": {
      "doc": "Links to a `Filter.Root` by its `id`. When omitted, the id is inherited from the nearest `Filter.Root` or `Filter.Content`.",
      "type": "string",
      "status": "optional"
    }
  }
}
```


## Filter.useFilter(id)

Returns an object with the following properties:


```json
{
  "props": {
    "filters": {
      "doc": "Object containing all active filters keyed by filterKey.",
      "type": "Record<string, FilterValue>",
      "status": "required"
    },
    "search": {
      "doc": "The current search string.",
      "type": "string",
      "status": "required"
    },
    "hasActiveFilters": {
      "doc": "`true` when any filter or search text is active.",
      "type": "boolean",
      "status": "required"
    },
    "resetFilters": {
      "doc": "Clears all filters and search text.",
      "type": "() => void",
      "status": "required"
    },
    "removeFilter": {
      "doc": "Removes a single filter by its filterKey.",
      "type": "(filterKey: string) => void",
      "status": "required"
    }
  }
}
```


## Filter.useFilterAsync(id, fetcher, options?)

Parameters:


```json
{
  "props": {
    "id": {
      "doc": "The `id` of the `Filter.Root` to link to.",
      "type": "string",
      "status": "required"
    },
    "fetcher": {
      "doc": "Async function called whenever filters or search change. Receives `{ filters, search }` and should return the filtered data.",
      "type": "(params: { filters, search }) => Promise<T>",
      "status": "required"
    },
    "options.initialData": {
      "doc": "Data to return before the first fetch completes.",
      "type": "T",
      "status": "optional"
    },
    "options.debounce": {
      "doc": "Delay in milliseconds before executing the fetcher after a state change. Useful for reducing API calls while the user is typing.",
      "type": "number",
      "status": "optional"
    }
  }
}
```


Returns an object with:


```json
{
  "props": {
    "data": {
      "doc": "The data returned by the fetcher. `undefined` until the first fetch completes (unless `initialData` is provided).",
      "type": [
        "T",
        "undefined"
      ],
      "status": "required"
    },
    "loading": {
      "doc": "`true` while a fetch is in progress.",
      "type": "boolean",
      "status": "required"
    },
    "error": {
      "doc": "The error thrown by the fetcher, if any. Reset to `undefined` on each new fetch.",
      "type": [
        "Error",
        "undefined"
      ],
      "status": "required"
    }
  }
}
```


## Translations


```json
{
  "locales": [
    "da-DK",
    "en-GB",
    "nb-NO",
    "sv-SE"
  ],
  "entries": {
    "Filter.activeFiltersCountLabel": {
      "nb-NO": "%s aktive filtre",
      "en-GB": "%s active filters",
      "sv-SE": "%s aktiva filter",
      "da-DK": "%s aktive filtre"
    },
    "Filter.activeFiltersLabel": {
      "nb-NO": "Aktive filtre",
      "en-GB": "Active filters",
      "sv-SE": "Aktiva filter",
      "da-DK": "Aktive filtre"
    },
    "Filter.applyFilterLabel": {
      "nb-NO": "Bruk filter",
      "en-GB": "Apply filter",
      "sv-SE": "Använd filter",
      "da-DK": "Anvend filter"
    },
    "Filter.ariaLabel": {
      "nb-NO": "Filter",
      "en-GB": "Filter",
      "sv-SE": "Filter",
      "da-DK": "Filter"
    },
    "Filter.cancelFilterLabel": {
      "nb-NO": "Avbryt",
      "en-GB": "Cancel",
      "sv-SE": "Avbryt",
      "da-DK": "Annuller"
    },
    "Filter.clearAllLabel": {
      "nb-NO": "Fjern alle",
      "en-GB": "Clear all",
      "sv-SE": "Rensa alla",
      "da-DK": "Fjern alle"
    },
    "Filter.dateLabel": {
      "nb-NO": "Dato",
      "en-GB": "Date",
      "sv-SE": "Datum",
      "da-DK": "Dato"
    },
    "Filter.hideFilterLabel": {
      "nb-NO": "Skjul filter",
      "en-GB": "Hide filter",
      "sv-SE": "Dölj filter",
      "da-DK": "Skjul filter"
    },
    "Filter.noResultsMessage": {
      "nb-NO": "Prøv å endre eller fjerne noen filtre.",
      "en-GB": "Try changing or removing some filters.",
      "sv-SE": "Försök att ändra eller ta bort några filter.",
      "da-DK": "Prøv at ændre eller fjerne nogle filtre."
    },
    "Filter.panelButtonLabel": {
      "nb-NO": "Filter",
      "en-GB": "Filter",
      "sv-SE": "Filter",
      "da-DK": "Filter"
    },
    "Filter.resultCountMessage": {
      "nb-NO": "%s treff",
      "en-GB": "%s result(s)",
      "sv-SE": "%s resultat",
      "da-DK": "%s resultat(er)"
    },
    "Filter.sortButtonLabel": {
      "nb-NO": "Sorter",
      "en-GB": "Sort",
      "sv-SE": "Sortera",
      "da-DK": "Sortér"
    }
  }
}
```

## Filter.Root Events


```json
{
  "props": {
    "onChange": {
      "doc": "Called whenever the applied filter state changes. Receives a `FilterChangeState` object with `search` and `filters`. When `behavior=\"manual\"`, this is called immediately for search changes and applied filter removals, while draft panel filter changes are only emitted when the user applies them.",
      "type": "(state: FilterChangeState) => void",
      "status": "optional"
    }
  }
}
```


## Filter.Search Events


```json
{
  "props": {
    "onChange": {
      "doc": "Called when the search input value changes. Receives the new value string.",
      "type": "(value: string) => void",
      "status": "optional"
    }
  }
}
```


## Filter.ActiveFilters Events


```json
{
  "props": {
    "onRemove": {
      "doc": "Called when a filter tag is removed by the user. Receives the `filterKey` of the removed filter.",
      "type": "(filterKey: string) => void",
      "status": "optional"
    }
  }
}
```


## Filter.SortButton Events


```json
{
  "props": {
    "onChange": {
      "doc": "Called when the user selects a sort option. Receives the Dropdown change event.",
      "type": "(event: DrawerListChangeEvent) => void",
      "status": "optional"
    }
  }
}
```
