Filter
Let users narrow a collection by composing property, operator and value conditions.
Installation
- Install the package.
npm install @hashintel/ds-components- Import the component.
import { Filter } from "@hashintel/ds-components";
export const NameFilter = () => (
<Filter<{ contains: string }>
property="name"
propertyLabel="Name"
operators={[
{ key: "contains", label: "contains", input: { type: "string" } },
]}
onChange={(key, value) => {
// apply the committed condition to your query
}}
/>
);When to use
- Do: Use Filter to let users narrow a table, list or graph of entities by user defined conditions and operations.
- Do: Use FilterGroup whenever chips appear together or alongside add/clear controls — it handles layout, fresh-chip focus and abandoned-draft cleanup.
- Do: Use
type: "select"inputs for values from a known set, reserve text and number inputs for free-form values.
When not to use
- Don't: Don't use a filter chip for only a few available filtering choices — use Select, SegmentedControl, Checkbox etc.
- Don't: Don't use chips for free-text search across the whole collection — pair the filter bar with a search TextInput instead.
- Don't: Don't use Filter to display read-only tokens or counts — use Chip.
Anatomy
A Filter renders as one chip of segments — property label, operator
dropdown, the operator's value input(s), and an optional remove button —
while FilterGroup lays chips out with its pinned action buttons:
<FilterGroup>
<Filter /> {/* [ property | operator ▾ | value input(s) | ✕ ] */}
<FilterGroup.AddFilter />
<FilterGroup.ClearFilters />
</FilterGroup>Examples
Basic
Pick an operator, fill its input, and press Enter (or leave the chip) to commit — onChange receives the operator key and value, or (key, null) when the inputs are cleared. Group operators with { id, label, items } entries when the menu grows long.
Input types
An operator's input declares what its value type needs: string renders a text input, number/int/float a number input, and null none — selecting such an operator commits immediately.
Select values
Use type: "select" when values come from a known set — single by default, multiple: true for muliple select. searchable adds a search bar to the dropdown, and overflow: "summary" keeps a long selection to a count instead of a comma list.
Async options
A select's items can be an async loader: it runs once when the operator's input mounts, the select shows its loading state meanwhile, and a failed load surfaces the select's emptyState. A committed value stays visible while options load.
Validation errors
Validate on commit and pass messages via errors: the chip turns red and shows them in a tooltip on hover and focus. Errors are a rendered state, not a gate.
Disabled
disabled freezes the property, operator and inputs but deliberately keeps the remove button active, so an obsolete filter can still be taken away. The remove button can optionally be disabled seperately.
Sizes
sm is the default, sized for filter bars above tables. Use md/lg in page-level toolbars, and reserve xxs/xs for dense chrome.
Switching the property
propertyMenu turns the property segment into a menu trigger — typically to switch the chip to another property — while propertyOnClick makes it a plain button. They are mutually exclusive; without either, the segment is static text.
Removing filters
removeable adds a close button whose onRemove fires when clicked. Leave it unset for fixed filters the user can't take away.
Grouping filters
FilterGroup lays chips out in a wrapping row and provides matching preset action buttons: FilterGroup.AddFilter and FilterGroup.ClearFilters. The group also handles focus states and animations for chips and buttons inside it.
import { useState } from "react";
import {
Filter,
FilterGroup,
type FilterOperator,
type FilterValue,
} from "@hashintel/ds-components";
type NameFilters = { contains: string };
const nameOperators: FilterOperator<NameFilters>[] = [
{ key: "contains", label: "contains", input: { type: "string" } },
];
export const EntityFilterBar = () => {
const [nameFilter, setNameFilter] = useState<FilterValue<NameFilters> | null>(
null,
);
const [showNameChip, setShowNameChip] = useState(false);
const clear = () => {
setShowNameChip(false);
setNameFilter(null);
};
return (
<FilterGroup>
{showNameChip && (
<Filter<NameFilters>
property="name"
propertyLabel="Name"
operators={nameOperators}
value={nameFilter}
onChange={(key, value) => setNameFilter({ key, value })}
removeable={{ onRemove: clear }}
/>
)}
<FilterGroup.AddFilter
renderAs="plusLabel"
onClick={() => setShowNameChip(true)}
/>
<FilterGroup.ClearFilters disabled={!showNameChip} onClick={clear} />
</FilterGroup>
);
};Dismissing abandoned filters
With dismissAbandoned, a removeable chip left incomplete (no operator, or any input empty) when the user clicks or focuses outside the group waits a short amount of time, then removes itself via onRemove. Any interaction with the group rescues it. Complete chips and input-less operators ("is empty") are never dismissed.
<FilterGroup dismissAbandoned>{/* chips and action buttons */}</FilterGroup>Behavior
- Width: the chip fits its content up to 100% of its container; each value input grows with its text and caps at 32ch, then ellipsizes. Truncated segments reveal their full text as a native tooltip.
- Number inputs reject invalid characters, and on commit, drafts are tidied ("5." becomes 5).
Content
- Write
propertyLabelas a 1–3 word noun phrase in sentence case: "Name", "Entity type", "Created by". - Write operator labels in lowercase, phrased to read as a sentence after the property: "contains", "is any of", "is at least".
- Keep
labelplain text even whenrenderItemadds rich content — the dropdown's typeahead and the inputs' accessible names read it. - Placeholders should show an example or unit, never an instruction: "Lat", "days" — not "Enter a value".
- Error messages should follow "[Property] must be [constraint]": "Score must be 100 or less".
Accessibility
- The chip is a
role="group"named"{propertyLabel} filter"; each value input is labelled"{propertyLabel} {operator} value"(numbered when there are several) and the remove button"Remove {propertyLabel} filter". errorssetaria-invalidon the inputs and surface on focus as well as hover.
Keyboard
| Key | Action |
|---|---|
| Enter | Commit a complete (or fully cleared) draft from any input; advance to the next input while the draft is incomplete |
| Escape | Restore the focused input's value from when it received focus; close an open dropdown without committing |
| ← / → | Move between the chip's segments; from a text input only once the caret sits at the matching edge |
What you must do
- Give every chip in a view a distinct
propertyLabel— all of its accessible names derive from it. - Validate committed values and pass
errors— beyond input constraints, the chip has no built-in validation. - Keep operator labels textual (see Content) so typeahead and input names stay meaningful.
API reference
Filter
| Prop | Type | Description |
|---|---|---|
| property | string | |
| propertyLabel | string | |
| operators | ItemOrGroup<FilterOperator<ValueMap>>[] | |
| onChange | (...change: FilterChange<ValueMap>) => void | Fires when a complete (or fully cleared) draft is committed |
| value | { key: keyof ValueMap & string; value: ValueMap[keyof ValueMap & string] | null; } | null default: null | |
| onInput | ((...change: FilterChange<ValueMap>) => void) | Fires on every edit, with the draft's value while it is complete and null otherwise |
| errors | string[] | Validation errors, shown in a tooltip below the filter on hover/focus |
| disabled | boolean | |
| size | "xxs" | "xs" | "sm" | "md" | "lg" default: "sm" | The size (height) of the element |
| autoFocus | boolean default: false | Focus the chip once on mount — its first input when an operator is pre-selected and takes input (an empty select input opens its dropdown too), otherwise the operator trigger. For chips created by a user action whose mount coincides with their container's — where FilterGroup's own fresh-chip focus treats them as restored state — so keyboard flow still lands inside the new chip. |
| removeable | false | { onRemove: () => void; } | onRemove fires when the user removes the chip via its ✕ button — and, inside a FilterGroup with dismissAbandoned, when the group dismisses the chip as abandoned. |
| propertyMenu | ItemOrGroup<MenuItem>[] | Renders the property segment as a menu trigger — typically for switching the filter to a different property, or other property-related actions. Mutually exclusive with propertyOnClick. |
| propertyOnClick | (() => void) | Renders the property segment as a button. Mutually exclusive with propertyMenu. |
FilterOperator
The shape of operators entries, optionally wrapped in { id, label, items }
groups. In consumer code it is typed as FilterOperator against your value
map, which derives each operator's input and onChange from its value type.
| Prop | Type | Description |
|---|---|---|
| key | string | Identifies the operator in value, onChange, and onInput |
| label | string | Plain-text name of the operator: the default rendering everywhere, and always what dropdown typeahead matches and the value inputs' aria-labels read. |
| input | LooseInputConfig | readonly (LooseInputConfig | InputSeparator)[] | null | The operator's value input(s): one input config, a tuple of configs optionally separated by static text or icon segments, or null for no input |
| renderItem | ReactNode default: label | Custom content for the operator's dropdown row. Defaults to label. |
| renderSelectedItem | ReactNode default: renderItem ?? label | Custom content for the chip's operator segment once this operator is selected. Defaults to renderItem, or label if neither is provided. |
| onChange | ((value: unknown) => void) | Called with this operator's committed value — null when its inputs are cleared |
TextInput
An operator input with type: "string" — a free-text value.
| Prop | Type | Description |
|---|---|---|
| type | "string" | Discriminant: a free-text value input |
| placeholder | string | Placeholder shown while the input is empty |
| min | number | Minimum value length forwarded to the native input |
| max | number | Maximum value length forwarded to the native input |
| pattern | string | Regex the value must match, as native pattern validation |
NumberInput
An operator input with a numeric type.
| Prop | Type | Description |
|---|---|---|
| type | "number" | "float" | "int" | Discriminant: a numeric value input — "int" rejects decimal input, "number"/"float" accept it |
| placeholder | string | Placeholder shown while the input is empty |
| min | number | Smallest accepted value |
| max | number | Largest accepted value |
| step | number | Granularity the value snaps to |
SingleSelectInput
An operator input with type: "select", picking one value from a dropdown.
| Prop | Type | Description |
|---|---|---|
| type | "select" | Discriminant: pick one value from a dropdown |
| items | SelectInputItems<SelectItem<string>> | The options, provided upfront or lazily via an async loader |
| renderItem | ((value: string) => ReactNode) | Custom renderer for options in the dropdown. Defaults to the option's text. |
| renderSelectedItem | ((value: string) => ReactNode) | Custom renderer for the selected value in the chip. Defaults to renderItem. |
| emptyState | ReactNode | Rendered in the dropdown when there are no options at all |
| placeholder | string | Chip segment text while no value is selected |
| multiple | false default: false | |
| searchable | boolean | { onSearch?: ((search: string) => void); } | Adds a search field to the dropdown; pass { onSearch } to filter externally |
MultiSelectInput
An operator input with type: "select" and multiple: true, picking
several values.
| Prop | Type | Description |
|---|---|---|
| type | "select" | Discriminant: pick several values from a dropdown |
| items | SelectInputItems<MultiSelectItem<string>> | The options, provided upfront or lazily via an async loader |
| multiple | true | |
| renderItem | ((value: string) => ReactNode) | Custom renderer for options in the dropdown. Defaults to the option's text. |
| renderSelectedItem | ((value: string) => ReactNode) | Custom renderer for each selected value in the chip. Defaults to renderItem. |
| renderSelectedAll | ((values: string[]) => ReactNode) | Custom renderer replacing the whole selected-values row in the chip |
| emptyState | ReactNode | Rendered in the dropdown when there are no options at all |
| placeholder | string | Chip segment text while no value is selected |
| maxItems | number | The maximum number of values that can be selected |
| overflow | "truncate" | "scroll" | "summary" | How the selected values render in the chip when they outgrow it |
| searchable | boolean | { onSearch?: ((search: string) => void); hideCount?: boolean; hideSelectAllToggle?: boolean | undefined; } | undefined | Adds a search field to the dropdown; the options object can filter externally and hide the count or the select-all toggle |
FilterGroup
| Prop | Type | Description |
|---|---|---|
| dismissAbandoned | boolean default: false | Auto-dismiss abandoned chips: once the user focuses or clicks outside the group while any member chip's draft is incomplete removes the chip after a delay |
FilterGroup.AddFilter
| Prop | Type | Description |
|---|---|---|
| onClick | MouseEventHandler<HTMLButtonElement> | |
| shape | "round" | "default" | The shape of the button. Non default shapes should very rarely be used |
| size | "xxs" | "xs" | "sm" | "md" | "lg" default: "sm" | The size (height) of the button |
| tone | "neutral" | "brand" | "error" | The color treatment of the button, e.g. for primary or destructive actions. |
| aria-label | string | Accessible name; required when there is no label or tooltip. Defines a string value that labels the current element. @see aria-labelledby. |
| tooltip | string | Tooltip shown on hover/focus; also names icon-only buttons |
| variant | "link" | "solid" | "subtle" | "ghost" | "linkSubtle" default: "ghost" | The overall visual style, from solid fill to inline link text; subtle and ghost sit between with a tinted or transparent background. |
| type | "button" | "submit" | "reset" | The native button type. Unlike a native <button>, it only submits forms when explicitly set to submit. |
| loading | boolean | Whether the button is in a loading state |
| pressed | boolean | Whether the button is in a pressed/active state |
| disabled | boolean | |
| tabIndex | number | |
| autoFocus | boolean | "never" | Whether to focus the element on mount; "never" prevents the element being focused automatically. |
| onMouseDown | MouseEventHandler<HTMLButtonElement> | |
| onMouseUp | MouseEventHandler<HTMLButtonElement> | |
| onMouseEnter | MouseEventHandler<HTMLButtonElement> | |
| onMouseLeave | MouseEventHandler<HTMLButtonElement> | |
| onKeyDown | KeyboardEventHandler<HTMLButtonElement> | |
| onFocus | FocusEventHandler<HTMLButtonElement> | |
| onBlur | FocusEventHandler<HTMLButtonElement> | |
| tooltipOptions | Omit<BaseTooltipProps & { variant?: "light" | "dark"; }, "children" | "content"> | Options forwarded to the tooltip shown when tooltip is set |
| renderAs | "plus" | "plusLabel" | "filterIcon" default: "plus" | What fills the button: a plus glyph (default), the plus glyph with an "Add filter" label, or the filter icon |
FilterGroup.ClearFilters
| Prop | Type | Description |
|---|---|---|
| onClick | MouseEventHandler<HTMLButtonElement> | |
| shape | "round" | "default" | The shape of the button. Non default shapes should very rarely be used |
| size | "xxs" | "xs" | "sm" | "md" | "lg" default: "sm" | The size (height) of the button |
| tone | "neutral" | "brand" | "error" | The color treatment of the button, e.g. for primary or destructive actions. |
| aria-label | string | Accessible name; required when there is no label or tooltip. Defines a string value that labels the current element. @see aria-labelledby. |
| tooltip | string | Tooltip shown on hover/focus; also names icon-only buttons |
| variant | "link" | "solid" | "subtle" | "ghost" | "linkSubtle" default: "ghost" | The overall visual style, from solid fill to inline link text; subtle and ghost sit between with a tinted or transparent background. |
| type | "button" | "submit" | "reset" | The native button type. Unlike a native <button>, it only submits forms when explicitly set to submit. |
| loading | boolean | Whether the button is in a loading state |
| pressed | boolean | Whether the button is in a pressed/active state |
| disabled | boolean | |
| tabIndex | number | |
| autoFocus | boolean | "never" | Whether to focus the element on mount; "never" prevents the element being focused automatically. |
| onMouseDown | MouseEventHandler<HTMLButtonElement> | |
| onMouseUp | MouseEventHandler<HTMLButtonElement> | |
| onMouseEnter | MouseEventHandler<HTMLButtonElement> | |
| onMouseLeave | MouseEventHandler<HTMLButtonElement> | |
| onKeyDown | KeyboardEventHandler<HTMLButtonElement> | |
| onFocus | FocusEventHandler<HTMLButtonElement> | |
| onBlur | FocusEventHandler<HTMLButtonElement> | |
| tooltipOptions | Omit<BaseTooltipProps & { variant?: "light" | "dark"; }, "children" | "content"> | Options forwarded to the tooltip shown when tooltip is set |
Was this page helpful?