Select
A select displays a collapsible list of options and allows a user to select one of them
Usage
import { Select } from "@lenso/ui";"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { SelectExample, states } from "./select-example";export function Default() { return <SelectExample label="State" choices={states} />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Anatomy
import {Select, Label, Description, Header, ListBox, Separator} from "@lenso/ui";
export default () => ( <Select> <Label /> <Select.Trigger> <Select.Value /> <Select.ClearButton /> <Select.Indicator /> </Select.Trigger> <Description /> <Select.Popover> <ListBox> <ListBox.Item> <Label /> <Description /> <ListBox.ItemIndicator /> </ListBox.Item> <ListBox.Section> <Header /> <ListBox.Item> <Label /> </ListBox.Item> </ListBox.Section> </ListBox> </Select.Popover> </Select>);Examples
Variants
The Select component supports two visual variants:
primary(default) - Standard styling with shadow, suitable for most use casessecondary- Lower emphasis variant without shadow, suitable for use in Surface components
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { exampleStyles, SelectExample } from "./select-example";const choices = [ { value: "option1", label: "Option 1" }, { value: "option2", label: "Option 2" },];export function Variants() { return ( <div {...stylex.props(exampleStyles.stack)}> <SelectExample label="Primary variant" choices={choices} variant="primary" /> <SelectExample label="Secondary variant" choices={choices} variant="secondary" /> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Full Width
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { exampleStyles, SelectExample } from "./select-example";export function FullWidth() { return ( <div {...stylex.props(exampleStyles.wide, exampleStyles.stack)}> <SelectExample fluid label="Favorite Animal" choices={["Cat", "Dog", "Bird"].map((label) => ({ label, value: label.toLowerCase() }))} /> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Description
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { SelectExample, states } from "./select-example";export function WithDescription() { return ( <SelectExample label="State" choices={states} description="Select your state of residence" /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Required
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { Form } from "@base-ui/react/form";import * as stylex from "@stylexjs/stylex";import { countrySections, exampleStyles, SelectExample, states } from "./select-example";export function Required() { return ( <Form {...stylex.props(exampleStyles.field, exampleStyles.stack)} onSubmit={(event) => { event.preventDefault(); alert("Form submitted successfully!"); }} > <SelectExample fluid required name="state" label="State" choices={states} /> <SelectExample fluid required name="country" label="Country" placeholder="Select a country" choices={countrySections .flatMap((section) => section.items) .filter((item) => ["usa", "canada", "mexico", "uk", "france", "germany"].includes(item.value), )} /> <button type="submit" {...stylex.props(exampleStyles.action)}> Submit </button> </Form> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Disabled
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { countries, exampleStyles, SelectExample, states } from "./select-example";export function Disabled() { return ( <div {...stylex.props(exampleStyles.stack)}> <SelectExample disabled label="State" choices={states} defaultValue="california" /> <SelectExample disabled multiple label="Countries to Visit" choices={countries.slice(0, 6)} placeholder="Select countries" defaultValue={["argentina", "japan", "france"]} /> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Disabled Options
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { SelectExample } from "./select-example";const animals = ["Dog", "Cat", "Bird", "Kangaroo", "Elephant", "Tiger"].map((label) => ({ label, value: label.toLowerCase(), disabled: label === "Cat" || label === "Kangaroo",}));export function WithDisabledOptions() { return <SelectExample label="Animal" placeholder="Select an animal" choices={animals} />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Multiple Select
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { countries, SelectExample } from "./select-example";export function MultipleSelect() { return ( <SelectExample multiple label="Countries to Visit" placeholder="Select countries" choices={countries} /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Sections
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { countrySections, SelectExample } from "./select-example";export function WithSections() { return ( <SelectExample label="Country" placeholder="Select a country" choices={countrySections.flatMap((section) => section.items)} sections={countrySections} /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Controlled
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { useState } from "react";import { controlledStates, exampleStyles, SelectExample } from "./select-example";export function Controlled() { const [state, setState] = useState<string | null>("california"); return ( <div {...stylex.props(exampleStyles.compactStack)}> <SelectExample label="State (controlled)" choices={controlledStates} placeholder="Select a state" value={state} onValueChange={setState} /> <p {...stylex.props(exampleStyles.note)}> Selected: {controlledStates.find((item) => item.value === state)?.label || "None"} </p> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Controlled Multiple
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { useState } from "react";import { controlledStates, exampleStyles, SelectExample } from "./select-example";export function ControlledMultiple() { const [selected, setSelected] = useState<string[]>(["california", "texas"]); return ( <div {...stylex.props(exampleStyles.stack)}> <SelectExample multiple label="States (controlled multiple)" choices={controlledStates} placeholder="Select states" value={selected} onValueChange={setSelected} /> <p {...stylex.props(exampleStyles.note)}> Selected: {selected.length > 0 ? selected.join(", ") : "None"} </p> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Controlled Open State
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import * as stylex from "@stylexjs/stylex";import { useState } from "react";import { exampleStyles, SelectExample, states } from "./select-example";export function ControlledOpenState() { const [open, setOpen] = useState(false); return ( <div {...stylex.props(exampleStyles.stack)}> <SelectExample label="State" choices={states} open={open} onOpenChange={setOpen} /> <button type="button" {...stylex.props(exampleStyles.action)} onClick={() => setOpen(!open)}> {open ? "Close" : "Open"} Select </button> <p {...stylex.props(exampleStyles.note)}>Select is {open ? "open" : "closed"}</p> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Asynchronous Loading
"use client";/** * HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 * Modified: abortable native fetch and scroll pagination replace RAC collection/load-more. */import { Spinner } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { useCallback, useEffect, useRef, useState } from "react";import { exampleStyles, SelectExample } from "./select-example";interface PokemonPage { next: string | null; results: { name: string }[];}export function AsynchronousLoading() { const [pokemon, setPokemon] = useState<{ name: string }[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); const cursor = useRef<string | null>("https://pokeapi.co/api/v2/pokemon"); const controller = useRef<AbortController | null>(null); const load = useCallback(async () => { if (!cursor.current || controller.current) return; const request = new AbortController(); controller.current = request; setLoading(true); setError(false); try { const response = await fetch(cursor.current, { signal: request.signal }); if (!response.ok) throw new Error(`Pokemon request failed (${response.status})`); const page: PokemonPage = await response.json(); cursor.current = page.next; setPokemon((previous) => [...previous, ...page.results]); } catch { if (!request.signal.aborted) setError(true); } finally { if (controller.current === request) { controller.current = null; setLoading(false); } } }, []); useEffect(() => { void load(); return () => { controller.current?.abort(); controller.current = null; }; }, [load]); return ( <SelectExample label="Pick a Pokemon" placeholder="Select a Pokemon" choices={pokemon.map(({ name }) => ({ value: name, label: name }))} onPopoverScroll={(event) => { const element = event.currentTarget; if (element.scrollHeight - element.scrollTop - element.clientHeight < 48) void load(); }} footer={ <div {...stylex.props(exampleStyles.loading)}> {loading && ( <> <Spinner size="sm" /> <span {...stylex.props(exampleStyles.note)}>Loading more...</span> </> )} {!loading && cursor.current && ( <button type="button" {...stylex.props(exampleStyles.action)} onClick={() => void load()} > {error ? "Retry loading" : "Load more"} </button> )} </div> } /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Custom Indicator
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { ChevronsExpandVertical } from "@gravity-ui/icons";import { SelectExample, states } from "./select-example";export function CustomIndicator() { return ( <SelectExample label="State" choices={states} indicator={<ChevronsExpandVertical width={12} height={12} aria-hidden="true" />} /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Clear Button
Select.Trigger is a button, so a nested <button> cannot be used for clear. Compose Select.ClearButton inside the trigger instead — it renders as a non-button control and does not open the menu.
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { useState } from "react";import { SelectExample, states } from "./select-example";export function WithClearButton() { const [value, setValue] = useState<string | null>("california"); return ( <SelectExample label="State" choices={states} value={value} onValueChange={setValue} clear={value === null ? undefined : () => setValue(null)} /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Because ARIA treats the children of a button as presentational, the clear control cannot take focus. It is therefore aria-hidden and acts as a pointer affordance only. When a Select.ClearButton is composed, the trigger also clears on Backspace or Delete, which is how keyboard and screen reader users clear the selection. Both paths call onClear.
Custom Value
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { Avatar, AvatarImage, AvatarFallback } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { exampleStyles, SelectExample } from "./select-example";const users = [ { id: "1", name: "Bob", email: "[email protected]", color: "blue" }, { id: "2", name: "Fred", email: "[email protected]", color: "green" }, { id: "3", name: "Martha", email: "[email protected]", color: "purple" }, { id: "4", name: "John", email: "[email protected]", color: "red" }, { id: "5", name: "Jane", email: "[email protected]", color: "orange" },];function UserAvatar({ user, small = false }: { user: (typeof users)[number]; small?: boolean }) { return ( <Avatar size="sm" xstyle={small ? exampleStyles.smallAvatar : undefined}> <AvatarImage src={`https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/${user.color}.jpg`} alt="" /> <AvatarFallback>{user.name.charAt(0)}</AvatarFallback> </Avatar> );}export function CustomValue() { return ( <SelectExample label="User" placeholder="Select a user" choices={users.map((user) => ({ value: user.id, label: user.name, content: ( <div {...stylex.props(exampleStyles.row)}> <UserAvatar user={user} /> <div {...stylex.props(exampleStyles.details)}> <span>{user.name}</span> <span {...stylex.props(exampleStyles.note)}>{user.email}</span> </div> </div> ), }))} valueContent={(value) => { const user = users.find((item) => item.id === value); return user ? ( <span {...stylex.props(exampleStyles.row)}> <UserAvatar user={user} small /> <span>{user.name}</span> </span> ) : ( "Select a user" ); }} /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Render Function
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { SelectExample, states } from "./select-example";export function RenderFunction() { return <SelectExample customRender label="State" choices={states} />;}Local adaptation source above. Derived from HeroUI v3.2.6 source.
In Surface
When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { Form } from "@base-ui/react/form";import * as stylex from "@stylexjs/stylex";import { countrySections, exampleStyles, SelectExample, states } from "./select-example";export function OnSurface() { return ( <div {...stylex.props(exampleStyles.surface)}> <Form {...stylex.props(exampleStyles.stack)} onSubmit={(event) => { event.preventDefault(); alert("Form submitted successfully!"); }} > <SelectExample fluid required name="state" label="State" choices={states} variant="secondary" /> <SelectExample fluid required name="country" label="Country" placeholder="Select a country" choices={countrySections .flatMap((section) => section.items) .filter((item) => ["usa", "canada", "mexico", "uk", "france", "germany"].includes(item.value), )} variant="secondary" /> <button type="submit" {...stylex.props(exampleStyles.action)}> Submit </button> </Form> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Customization
Tailwind CSS
"use client";/** HeroUI v3.2.6 adaptation. Copyright 2026 HeroUI. SPDX-License-Identifier: Apache-2.0 */import { SelectExample } from "./select-example";export function CustomStyles() { return ( <SelectExample custom label="Plan" placeholder="Pick a plan" variant="secondary" choices={[ { value: "free", label: "Free" }, { value: "pro", label: "Pro" }, ]} /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Global CSS
To customize the Select component classes, you can use the @layer components directive.
@layer components { .select { @apply flex flex-col gap-1; }
.select__trigger { @apply rounded-lg border border-border bg-surface p-2; }
.select__value { @apply text-current; }
.select__indicator { @apply text-muted; }
.select__popover { @apply rounded-lg border border-border bg-surface p-2; }}Styling Reference
HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
CSS Classes
The Select component uses these CSS classes (View source styles):
Base Classes [!toc]
.select- Base select container.select__trigger- The button that triggers the select.select__value- The displayed value or placeholder.select__clear-button- The optional clear control inside the trigger.select__indicator- The dropdown indicator icon.select__popover- The popover container
Variant Classes [!toc]
.select--primary- Primary variant with shadow (default).select--secondary- Secondary variant without shadow, suitable for use in surfaces
State Classes [!toc]
.select[data-invalid="true"]- Invalid state.select__trigger[data-focus-visible="true"]- Focused trigger state.select__trigger[data-disabled="true"]- Disabled trigger state.select__value[data-placeholder="true"]- Placeholder state.select__clear-button[data-empty="true"]- Clear button hidden when no selection.select__indicator[data-open="true"]- Open indicator state
Interactive States
The component supports both CSS pseudo-classes and data attributes for flexibility:
- Hover:
:hoveror[data-hovered="true"]on trigger - Focus:
:focus-visibleor[data-focus-visible="true"]on trigger - Disabled:
:disabledor[data-disabled="true"]on select - Open:
[data-open="true"]on indicator
API Reference
Select
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | 'Select an item' | Temporary text that occupies the select when it is empty |
selectionMode | "single" | "multiple" | "single" | Whether single or multiple selection is enabled |
isOpen | boolean | - | Sets the open state of the menu (controlled) |
defaultOpen | boolean | - | Sets the default open state of the menu (uncontrolled) |
onOpenChange | (isOpen: boolean) => void | - | Handler called when the open state changes |
disabledKeys | Iterable<Key> | - | Keys of disabled items |
isDisabled | boolean | - | Whether the select is disabled |
value | Key | Key[] | null | - | Current value (controlled) |
defaultValue | Key | Key[] | null | - | Default value (uncontrolled) |
onChange | (value: Key | Key[] | null) => void | - | Handler called when the value changes |
onClear | () => void | - | Handler called when the selection is cleared |
isRequired | boolean | - | Whether user input is required |
isInvalid | boolean | - | Whether the select value is invalid |
name | string | - | The name of the input, used when submitting an HTML form |
autoComplete | string | - | Describes the type of autocomplete functionality |
fullWidth | boolean | false | Whether the select should take full width of its container |
variant | "primary" | "secondary" | "primary" | Visual variant of the component. primary is the default style with shadow. secondary is a lower emphasis variant without shadow, suitable for use in surfaces. |
className | string | - | Additional CSS classes |
children | ReactNode | RenderFunction | - | Select content or render function |
render | DOMRenderFunction<keyof React.JSX.IntrinsicElements, SelectRenderProps> | - | Overrides the default DOM element with a custom render function. |
Select.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | RenderFunction | - | Trigger content or render function |
Select.Value
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | RenderFunction | - | Value content or render function |
render | DOMRenderFunction<keyof React.JSX.IntrinsicElements, SelectValueRenderProps> | - | Overrides the default DOM element with a custom render function. |
Select.Indicator
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | - | Custom indicator content |
Select.ClearButton
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | ReactNode | - | Custom content, replacing the default icon |
onClick | (e: MouseEvent) => void | - | Handler called when the control is clicked |
Select.ClearButton renders as a span so it can live inside Select.Trigger (a button) without nesting buttons. It is visually hidden when the selection is empty.
Select.Popover
| Prop | Type | Default | Description |
|---|---|---|---|
placement | "bottom" | "bottom left" | "bottom right" | "bottom start" | "bottom end" | "top" | "top left" | "top right" | "top start" | "top end" | "left" | "left top" | "left bottom" | "start" | "start top" | "start bottom" | "right" | "right top" | "right bottom" | "end" | "end top" | "end bottom" | "bottom" | Placement of the popover relative to the trigger |
className | string | - | Additional CSS classes |
children | ReactNode | - | Content children |
Render Props
When using render functions with Select.Value, these values are provided:
| Prop | Type | Description |
|---|---|---|
defaultChildren | ReactNode | The default rendered value |
isPlaceholder | boolean | Whether the value is a placeholder |
state | SelectState | The state of the select |
selectedItems | Node[] | The currently selected items |
Accessibility
The Select component implements the ARIA listbox pattern and provides:
- Full keyboard navigation support
- Screen reader announcements for selection changes
- Proper focus management
- Support for disabled states
- Typeahead search functionality
- HTML form integration
For more information, see the React Aria Select documentation.