Select

Let users pick one or more options from a predefined list.

Installation

  1. Install the package.
npm install @hashintel/ds-components
  1. Import the component.
import { useState } from "react";

import { Select } from "@hashintel/ds-components";

export const EntityTypePicker = () => {
  const [entityType, setEntityType] = useState<string | null>(null);

  return (
    <Select
      placeholder="Select an entity type…"
      items={[
        { value: "person", text: "Person" },
        { value: "organization", text: "Organization" },
        { value: "document", text: "Document" },
      ]}
      value={entityType}
      onChange={setEntityType}
    />
  );
};

When to use

  • Do: Use Select to pick one value from multiple predefined options.
  • Do: Use multiple when several values apply together, such as table columns or entity types to filter by.
  • Do: Use searchable once a list grows past roughly 10 options, so users filter instead of scrolling.
  • Do: Use Select in forms — name submits the value through a hidden native <select>.

When not to use

  • Don't: Don't use Select to trigger actions — options are values, not commands. Use Menu.
  • Don't: Don't use Select for an on/off setting — use Toggle, or Checkbox in forms.
  • Don't: Don't use Select when typing is the primary way in, such as huge or asynchronously searched datasets — use Combobox, a text input first.
  • Don't: Avoid using Select for 3 or less choices — use RadioGroup in forms or SegmentedControl in toolbars.

Anatomy

Select is a single component — its parts are configured through props and the items data rather than composed as JSX parts:

  • trigger — the field that opens the dropdown and anchors it, showing the placeholder or the current selection, with an optional prefix and a clear button while the selection is clearable.
  • items — the dropdown's content: options ({ value, text }), mixed freely with labelled groups. In a multi select, items additionally take suffix, showOnlyButton, selectedStyle and selectedTone — see Item suffixes and selection styles.
  • search field — pinned atop the dropdown when searchable, filtering the items by their text.
  • selection summary — in a searchable multi select, an "x of y" count and a "Select all" / "Clear all" toggle beneath the options.
<Select
  placeholder="Select a property type…" // trigger text before a selection
  prefix={{ iconName: "search" }} // leading icon, text or custom node
  searchable // search field atop the dropdown
  items={[
    { value: "text", text: "Text" }, // an option
    {
      id: "numeric",
      label: "Numeric", // a group heading (not selectable)
      items: [
        { value: "number", text: "Number" },
        { value: "integer", text: "Integer" },
      ],
    },
  ]}
  value={value}
  onChange={setValue}
/>

Examples

Basic

A controlled input: pass items, the selected value, and onChange. Declare items as const to narrow value and onChange to the declared item values.

Clearing and optional selection

A single select without required is always clearable: a clear button shows on hover and focus while a value is selected, and clearing calls onChange with null. required removes the button but it can be reintroduced by setting clearable.

A multi select only displays the clear button if clearable is set.

Disabled and invalid

Disabled selects grey out and ignore interaction — prefer readonly when the value still matters to the reader. Set invalid for validation failures and pair it with a Form.Field error message so the reason is visible.

Read-only

readonly strips the input chrome and renders the selection as plain text. Use it for view modes, so the layout holds steady when editing toggles.

Person
Name, Status

Loading

loading shows a spinner in the trigger and "Loading options…" in the dropdown while items is still empty; the control stays interactive throughout.

Sizes

md is the default. Use xxs–sm in dense chrome such as filter bars and table rows, and lg for spacious forms — size also scales the dropdown list.

Variants

default is the bordered input. subtle drops the border until hover, for dense or inline chrome; naked strips all input chrome and inherits the surrounding text styles (size still sizes the dropdown) — the host supplies any focus affordance.

Widths

Preset widths align selects with other form inputs: xs–lg are fixed, fullWidth (the default) fills the container, and fitContent hugs the selection. Size with CSS instead when alignment with other inputs doesn't matter.

With prefix

prefix places an adornment at the start of the trigger: { iconName } for an icon, { text } for a short string, or { content } for a custom node.

Sort:

Grouped options

Group related options with { id, label, items } entries — group labels are headings, not selectable rows. Group only when the categories are obvious; a flat list scans faster when it's short.

Custom rendering

renderItem controls how options render in the dropdown and doubles as the trigger's renderer; renderSelectedItem overrides the trigger alone. Keep the item's text in the output so options keep their accessible names.

In a multi select, renderSelectedItem renders each value in the trigger's row, and renderSelectedAll replaces the entire selection row — along with the overflow behavior.

Searchable

searchable adds a search field to the top of the dropdown that filters options by their text — every space-separated term must match. Closing the dropdown resets the search; pass { onSearch } to react to the query, e.g. to fetch options.

Multiple selection

multiple keeps the dropdown open while values toggle — each toggle fires onChange, and Escape reverts the selection to its state when the dropdown opened. A searchable multi select adds an "x of y" count and a Select all / Clear all toggle beneath the options — hide either with hideCount / hideSelectAllToggle.

Selected-value overflow

In multiple select, overflow sets how the trigger shows many values: scroll (the default) scrolls the row horizontally, truncate stops at a "+X" badge, and summary joins the names — falling back to "X of Y" once they no longer fit, or "any" when everything is selected.

Limiting selection

In multiple select, maxItems caps the selection: at the cap, unselected options disable until a value is deselected. The Select all toggle hides when the cap puts selecting every enabled option out of reach.

Item suffixes and selection styles

Multi-select items can take a suffix aligned to the right, and showOnlyButton can swap it for an "Only" button on hover that narrows the selection to that item. Items indicate selection with a checkbox by default — restyle per item with selectedStyle (checkbox, tick, highlight) and selectedTone.

Connected inputs

Set connectToRightInput on the left input and connectToLeftInput on the right to join a pair into one visual control. Only the default variant connects — subtle and readonly inputs render separately.

In forms

Inside a Form.Field the label wires to the trigger automatically, and name submits the selected value through a hidden native <select>.

Behavior

  • The menu's content mounts renders in a portal above the page, preventing most z-index + overflow issues.
  • Selected values missing from items render as disabled options at the top of the list, so a committed selection always stays visible — except while loading with no options yet.
  • "Select all" selects every enabled option — regardless of the active search filter.
  • defaultOpen is read once on mount and doesn't fire onOpenChange.

Content

  • Option text is a 1–4 word noun phrase, sentence case, no punctuation: "Person", "Created at", "Last 30 days". Name values, not actions — actions belong in Menu.
  • Keep option text unique and front-load the distinguishing word, so the list scans and typeahead works.
  • Order options by the likeliest choice, numerically, or alphabetically — one scheme per list.
  • Placeholders follow "Select a [noun]…" and never carry requirements — they disappear once a value is chosen.
  • Group labels are 1–2 word category nouns: "System types", "Numeric".
  • Write emptyState as why plus next step: "No properties yet — add one in the type editor."

Accessibility

Select implements the WAI-ARIA listbox pattern,

What Select provides

  • A real <button> trigger with listbox popup semantics, arrow-key navigation and typeahead.
  • A hidden native <select> mirrors the selection so forms submit it under name.
  • Inside a Form.Field the trigger adopts the field's label id automatically.
  • The dropdown's search field and the clear button carry built-in labels ("Search options", "Clear input").

What you must do

  • Give every select an accessible name: a Form.Field label, or aria-label where no label is visible (toolbars, naked selects). A placeholder is not a label.
  • With variant="naked", supply your own focus affordance — the variant strips hover and focus styles.
  • Keep the item's text in renderItem output and mark decorative nodes aria-hidden, so options keep their accessible names.

Keyboard

Select adds handling beyond the listbox pattern:

KeyAction
Escape (multi select)Closes the dropdown and reverts the selection to its state when it opened.

API reference

Select

PropTypeDescription
items
readonly ItemOrGroup<SelectItem<TValue>>[] |⁠ readonly ItemOrGroup<MultiSelectItem<TValue>>[]
value
string |⁠ readonly NoInfer<TValue>[] |⁠ null |⁠ undefined
onChange
((value: NoInfer<TValue>) => void) |⁠ ((value: NoInfer<TValue> |⁠ null) => void) |⁠ ((value: NoInfer<TValue> |⁠ null) => void) |⁠ ((value: NoInfer<TValue>[]) => void)
placeholder
string
readonly
boolean
loading
boolean
variant
"subtle" |⁠ "default" |⁠ "naked"
default: "default"
align
"left" |⁠ "right" |⁠ "center"
default: "left"
width
"xs" |⁠ "sm" |⁠ "md" |⁠ "lg" |⁠ "fullWidth" |⁠ "fitContent"
default: "fullWidth"
hideArrow
boolean
prefix
Prefix
connectToLeftInput
boolean
connectToRightInput
boolean
onClick
MouseEventHandler<Element>
onKeyDown
KeyboardEventHandler<Element>
tabIndex
number
renderItem
((value: TValue) => ReactNode)
emptyState
ReactNode
onOpenChange
((open: boolean) => void)
defaultOpen
boolean
size
"xxs" |⁠ "xs" |⁠ "sm" |⁠ "md" |⁠ "lg"
default: "md"
disabled
boolean
autoFocus
boolean |⁠ "never"
onFocus
FocusEventHandler<HTMLButtonElement>
onBlur
FocusEventHandler<HTMLButtonElement>
name
string
invalid
boolean
multiple
boolean
maxItems
number
overflow
"truncate" |⁠ "scroll" |⁠ "summary"
default: "scroll"
searchable
boolean |⁠ (SelectSearchable & { hideCount?: undefined; hideSelectAllToggle?: undefined; }) |⁠ (SelectSearchable & { hideCount?: boolean; hideSelectAllToggle?: boolean; }) |⁠ undefined
renderSelectedItem
((value: TValue) => ReactNode) |⁠ ((value: TValue) => ReactNode)
renderSelectedAll
((values: TValue[]) => ReactNode)
required
boolean
clearable
boolean |⁠ { onClear: () => void; }

SelectItem

The shape of items entries in a single select.

PropTypeDescription
value
string
text
string
disabled
boolean

MultiSelectItem

The shape of items entries in a multi select — SelectItem plus right-aligned suffix content and per-item selection styling; see Item suffixes and selection styles.

PropTypeDescription
value
string
text
string
disabled
boolean
suffix
ReactNode
showOnlyButton
boolean
selectedStyle
"checkbox" |⁠ "tick" |⁠ "highlight"
default: "checkbox"
selectedTone
"neutral" |⁠ "brand" |⁠ "error"
default: "neutral"

SelectGroup

A labelled group of options, usable wherever items are: { id, label, items }.

PropTypeDescription
id
string
label
ReactNode
items
SelectItem<string>[]

Was this page helpful?

On this page