Filter

Let users narrow a collection by composing property, operator and value conditions.

Name
Entity type
Properties

Installation

  1. Install the package.
npm install @hashintel/ds-components
  1. 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.

Name
Name

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.

Name
Properties
Archived

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.

Entity type
Entity type

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.

Created byis

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.

Scoreis above

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.

Entity type

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.

xxs
xs
sm
md
lg

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.

Entity type
Archived

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.

Name
Entity type
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 propertyLabel as 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 label plain text even when renderItem adds 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".
  • errors set aria-invalid on the inputs and surface on focus as well as hover.

Keyboard

KeyAction
EnterCommit a complete (or fully cleared) draft from any input; advance to the next input while the draft is incomplete
EscapeRestore 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

PropTypeDescription
property
string
propertyLabel
string
operators
ItemOrGroup<FilterOperator<ValueMap>>[]
onChange
(...change: FilterChange<ValueMap>) => void
value
{ key: keyof ValueMap & string; value: ValueMap[keyof ValueMap & string] |⁠ null; } |⁠ null
default: null
onInput
((...change: FilterChange<ValueMap>) => void)
errors
string[]
disabled
boolean
size
"xxs" |⁠ "xs" |⁠ "sm" |⁠ "md" |⁠ "lg"
default: "sm"
autoFocus
boolean
default: false
removeable
false |⁠ { onRemove: () => void; }
propertyMenu
ItemOrGroup<MenuItem>[]
propertyOnClick
(() => void)

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.

PropTypeDescription
key
string
label
string
input
LooseInputConfig |⁠ readonly (LooseInputConfig |⁠ InputSeparator)[] |⁠ null
renderItem
ReactNode
default: label
renderSelectedItem
ReactNode
default: renderItem ?? label
onChange
((value: unknown) => void)

TextInput

An operator input with type: "string" — a free-text value.

PropTypeDescription
type
"string"
placeholder
string
min
number
max
number
pattern
string

NumberInput

An operator input with a numeric type.

PropTypeDescription
type
"number" |⁠ "float" |⁠ "int"
placeholder
string
min
number
max
number
step
number

SingleSelectInput

An operator input with type: "select", picking one value from a dropdown.

PropTypeDescription
type
"select"
items
SelectInputItems<SelectItem<string>>
renderItem
((value: string) => ReactNode)
renderSelectedItem
((value: string) => ReactNode)
emptyState
ReactNode
placeholder
string
multiple
false
default: false
searchable
boolean |⁠ { onSearch?: ((search: string) => void); }

MultiSelectInput

An operator input with type: "select" and multiple: true, picking several values.

PropTypeDescription
type
"select"
items
SelectInputItems<MultiSelectItem<string>>
multiple
true
renderItem
((value: string) => ReactNode)
renderSelectedItem
((value: string) => ReactNode)
renderSelectedAll
((values: string[]) => ReactNode)
emptyState
ReactNode
placeholder
string
maxItems
number
overflow
"truncate" |⁠ "scroll" |⁠ "summary"
searchable
boolean |⁠ { onSearch?: ((search: string) => void); hideCount?: boolean; hideSelectAllToggle?: boolean |⁠ undefined; } |⁠ undefined

FilterGroup

PropTypeDescription
dismissAbandoned
boolean
default: false

FilterGroup.AddFilter

PropTypeDescription
onClick
MouseEventHandler<HTMLButtonElement>
shape
"round" |⁠ "default"
size
"xxs" |⁠ "xs" |⁠ "sm" |⁠ "md" |⁠ "lg"
default: "sm"
tone
"neutral" |⁠ "brand" |⁠ "error"
aria-label
string
tooltip
string
variant
"link" |⁠ "solid" |⁠ "subtle" |⁠ "ghost" |⁠ "linkSubtle"
default: "ghost"
type
"button" |⁠ "submit" |⁠ "reset"
loading
boolean
pressed
boolean
disabled
boolean
tabIndex
number
autoFocus
boolean |⁠ "never"
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">
renderAs
"plus" |⁠ "plusLabel" |⁠ "filterIcon"
default: "plus"

FilterGroup.ClearFilters

PropTypeDescription
onClick
MouseEventHandler<HTMLButtonElement>
shape
"round" |⁠ "default"
size
"xxs" |⁠ "xs" |⁠ "sm" |⁠ "md" |⁠ "lg"
default: "sm"
tone
"neutral" |⁠ "brand" |⁠ "error"
aria-label
string
tooltip
string
variant
"link" |⁠ "solid" |⁠ "subtle" |⁠ "ghost" |⁠ "linkSubtle"
default: "ghost"
type
"button" |⁠ "submit" |⁠ "reset"
loading
boolean
pressed
boolean
disabled
boolean
tabIndex
number
autoFocus
boolean |⁠ "never"
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">

Was this page helpful?

On this page