Select
Let users pick one or more options from a predefined list.
Installation
- Install the package.
npm install @hashintel/ds-components- 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
multiplewhen several values apply together, such as table columns or entity types to filter by. - Do: Use
searchableonce a list grows past roughly 10 options, so users filter instead of scrolling. - Do: Use Select in forms —
namesubmits 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
placeholderor the current selection, with an optionalprefixand 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 takesuffix,showOnlyButton,selectedStyleandselectedTone— 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.
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.
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
itemsrender as disabled options at the top of the list, so a committed selection always stays visible — except whileloadingwith no options yet. - "Select all" selects every enabled option — regardless of the active search filter.
defaultOpenis read once on mount and doesn't fireonOpenChange.
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
emptyStateas 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 undername. - Inside a
Form.Fieldthe 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.Fieldlabel, oraria-labelwhere no label is visible (toolbars,nakedselects). 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
renderItemoutput and mark decorative nodesaria-hidden, so options keep their accessible names.
Keyboard
Select adds handling beyond the listbox pattern:
| Key | Action |
|---|---|
| Escape (multi select) | Closes the dropdown and reverts the selection to its state when it opened. |
API reference
Select
| Prop | Type | Description |
|---|---|---|
| items | readonly ItemOrGroup<SelectItem<TValue>>[] | readonly ItemOrGroup<MultiSelectItem<TValue>>[] | The options to pick from |
| value | string | readonly NoInfer<TValue>[] | null | undefined | The current selection, null/undefined while empty |
| onChange | ((value: NoInfer<TValue>) => void) | ((value: NoInfer<TValue> | null) => void) | ((value: NoInfer<TValue> | null) => void) | ((value: NoInfer<TValue>[]) => void) | Called with the selected value — null when the selection is cleared. |
| placeholder | string | |
| readonly | boolean | Whether editing is disabled. Unlike disabled, strips the input styles and renders the value as plain text. |
| loading | boolean | Whether a loading spinner is shown inside the input. |
| variant | "subtle" | "default" | "naked" default: "default" | The input's visual style: subtle removes the border so it reads as inline text; naked strips all input chrome and inherits the surrounding text styles (size then only affects the dropdown). |
| align | "left" | "right" | "center" default: "left" | The alignment of the text inside the input. |
| width | "xs" | "sm" | "md" | "lg" | "fullWidth" | "fitContent" default: "fullWidth" | A set of standard widths to choose for the input. You can also set the width with css when aligning with other inputs is not required. |
| hideArrow | boolean | Hide the dropdown arrow |
| prefix | Prefix | Element rendered at the start of the input. |
| connectToLeftInput | boolean | Whether this input renders as connected to the input on its left. Set the matching prop on both inputs; subtle and readonly inputs are never connected. |
| connectToRightInput | boolean | Whether this input renders as connected to the input on its right. Set the matching prop on both inputs; subtle and readonly inputs are never connected. |
| onClick | MouseEventHandler<Element> | |
| onKeyDown | KeyboardEventHandler<Element> | |
| tabIndex | number | |
| renderItem | ((value: TValue) => ReactNode) | Custom renderer for items in the dropdown. Defaults to the item's text. Note that if connectToLeftInput or connectToRightInput the height of the rendered selected item is clamped to the default height of select so that it correctly aligns. |
| emptyState | ReactNode | Optional custom message for scenarios where there are no items available to show |
| onOpenChange | ((open: boolean) => void) | Called when the dropdown opens or closes |
| defaultOpen | boolean | Mount with the dropdown already open. Read once on mount; it does not open or close the dropdown afterwards, and onOpenChange does not fire for this initial state. |
| size | "xxs" | "xs" | "sm" | "md" | "lg" default: "md" | The size (height) of the element |
| disabled | boolean | Set to show the input as disabled |
| autoFocus | boolean | "never" | Set to true to make the element focused on mount or 'never' to prevent the item being auto-focused |
| onFocus | FocusEventHandler<HTMLButtonElement> | The onFocus handler |
| onBlur | FocusEventHandler<HTMLButtonElement> | The onBlur handler |
| name | string | The name of the input |
| invalid | boolean | Set to show the input as invalid |
| multiple | boolean | Whether multiple values can be selected. |
| maxItems | number | The maximum number of values that can be selected. Once reached, unselected items are disabled until a value is deselected. |
| overflow | "truncate" | "scroll" | "summary" default: "scroll" | How the selected values render in the trigger when no renderSelectedAll is given: a row that scrolls horizontally, truncates with a "+X" badge, or summarises the names (falling back to "X of Y" once they no longer fit). |
| searchable | boolean | (SelectSearchable & { hideCount?: undefined; hideSelectAllToggle?: undefined; }) | (SelectSearchable & { hideCount?: boolean; hideSelectAllToggle?: boolean; }) | undefined | Set to add a search field to the dropdown that filters the items by their text. onSearch is called as the search value changes, including with "" when the dropdown closes and the search resets. |
| renderSelectedItem | ((value: TValue) => ReactNode) | ((value: TValue) => ReactNode) | Custom renderer for the selected value in the trigger. Defaults to renderItem, or the item's text if neither is provided. |
| renderSelectedAll | ((values: TValue[]) => ReactNode) | Custom renderer for the trigger's entire selection, replacing the per-value row and with it the overflow behaviour (and renderSelectedItem). |
| required | boolean | |
| clearable | boolean | { onClear: () => void; } | Whether a required select can be cleared, showing the clear button. |
SelectItem
The shape of items entries in a single select.
| Prop | Type | Description |
|---|---|---|
| value | string | Identifies the option; reported to onChange when selected |
| text | string | The option's visible label — shown in the trigger while selected, and matched by search and typeahead |
| disabled | boolean | Prevents selecting the option |
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.
| Prop | Type | Description |
|---|---|---|
| value | string | Identifies the option; reported to onChange when selected |
| text | string | The option's visible label — shown in the trigger while selected, and matched by search and typeahead |
| disabled | boolean | Prevents selecting the option |
| suffix | ReactNode | Optional content aligned to the right of the item in the dropdown |
| showOnlyButton | boolean | Show an "Only" button while the item is hovered, which sets the selection to just this item. It renders in the suffix position, replacing suffix while visible. |
| selectedStyle | "checkbox" | "tick" | "highlight" default: "checkbox" | How a selected item is shown |
| selectedTone | "neutral" | "brand" | "error" default: "neutral" | Tone of the selected indicator, when it differs from tone |
SelectGroup
A labelled group of options, usable wherever items are: { id, label, items }.
| Prop | Type | Description |
|---|---|---|
| id | string | Identifies the group |
| label | ReactNode | Rendered as an uppercase label above the group's options; an empty string draws a plain divider |
| items | SelectItem<string>[] | The group's options; groups don't nest |
Was this page helpful?