Import
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:
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.Rootinside the firstList.Item.Basic. Filtered results render as subsequent list items. This is the most common pattern. - Column layout with Grid — Use
Grid.ContainerwithGrid.Itemto place the filter and results side by side. UseFilter.Contentto link the results area to the filter viaconnectedTo.
Sub-components
Filter.Root— Root wrapper. Provides filter context and syncs state viauseSharedState. Requires a uniqueid. Supports spacing props.Filter.Header— Groups the filter controls (toolbar, panel, active filters) above the results. When used together withFilter.Contentcontaining aList.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 sharedsearchstring. Browser autocomplete, autocorrect, autocapitalize, and spellcheck are disabled by default.Filter.Toolbar— Horizontal row that wrapsFilter.SearchandFilter.Toolbar.Actions.Filter.Toolbar.Actions— Groups action buttons (e.g.Filter.PanelButton) for proper responsiveness.Filter.Panel— Expandable inline panel toggled byFilter.PanelButton. Renders filter children as tertiary accordions with a white background.Filter.PanelButton— Toggle button forFilter.Panel. Shows a filter icon when closed and a close icon when open. Accepts allButtonprops.Filter.ActiveFilters— Renders active filters as removableTagchips. Returns nothing when no filters are active. SetshowCategoryLabelto prefix each tag with its category name (e.g. "Betalingstype: Kort" instead of "Kort"). SetcollapsibleThresholdto 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. Inbehavior="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. SupportsdefaultOpento start expanded. Open/closed state is remembered across panel opens.Filter.Date— Built-in date range filter usingDatePicker. When placed inside aFilter.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 FormsMultiSelectioncomponent. 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 thesearchstring from the linked filter state and wraps matching substrings in<mark>tags. Can be linked viaconnectedToor inherits the id from the nearestFilter.RootorFilter.Content.Filter.Content— Wraps result content and shows aSkeletonloading state when the filter is loading. When used inside aFilter.Root, the id is inherited automatically. When used outside, link it viaconnectedTo. Supports spacing props.Filter.NoResults— Renders a translated "no results" message whenresultCountis0. When placed inside aList.Container, it automatically renders as a list item. Can be placed afterFilter.ActiveFiltersinside a container, or inside aFilter.Contentwhere it inheritsconnectedToautomatically.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. ReadsresultCountfrom the nearestFilter.Root, aconnectedToid, or aresultCountprop. Supports spacing props.
Hooks
Filter.useFilter(id)— Reads filter state from anywhere — does not need to be insideFilter.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 syncsresultLoading/resultCountto shared state. Options:initialDatafor 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 insideFilter.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'suseSearchParams. Pass the hook from your router version.Filter.useNextRouter(id, { useRouter, usePathname, useSearchParams, ...options })— Uses Next.js App Router hooks. PassuseRouter,usePathname, anduseSearchParamsfromnext/navigation.
Basic usage
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.Selectionlabel="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:
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.Selectionlabel="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:
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><Inputlabel="Min"onChange={({ value }) =>handleChange(value, current?.value?.max)}/><Inputlabel="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.
function TransactionList() {const { data } = Filter.useFilterAsync('my-filter',async ({ filters, search }) => {const res = await fetch(`/api/transactions?q=${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.
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
When the filter panel is closed — via the "Hide filter" button, the "Apply" button, or the "Cancel" button in manual mode — focus is automatically returned to the Filter.PanelButton. This ensures keyboard users don't lose their place in the page.
ARIA attributes
Filter.Rootrenders withrole="search"and anaria-labelto identify the filter region.Filter.PanelButtonusesaria-expandedto communicate whether the panel is open or closed.Filter.ActiveFiltersrenders a labeled group so screen readers can identify the active filter tags.
Relevant links
Demos
Basic usage
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.
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.
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.
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.
Predefined filters
Use defaultFilters to pre-select filters on mount. The panel and relevant filter accordions open automatically.
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'suseSearchParams.Filter.useNextRouter(id, { useRouter, usePathname, useSearchParams, excludeSearch? })— Uses Next.js navigation hooks.
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.
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.
Toolbar with actions only
A toolbar with only action buttons and no search field.
Search only
A simple search field with a secondary search button.
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.
Decoupled hook
Filter.useFilter(id) can be called anywhere in the tree — the filter UI and data consumer can live in completely separate components.