A select displays a collapsible list of options and allows a user to select one of them.
Import
import { Select } from "heroui-solid";Usage
import { Label, ListBox, Select } from "heroui-solid"
import { For } from "solid-js"
const states = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
export function Default() {
return (
<Select class="w-[256px]" placeholder="Select one">
<Label>State</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<For each={states}>
{(state) => (
<ListBox.Item id={state.id} textValue={state.name}>
{state.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Select.Popover>
</Select>
)
}Anatomy
Import the Select component and access all parts using dot notation.
import { Select, Label, Description, ListBox } from "heroui-solid";
export default () => (
<Select>
<Label />
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Description />
<Select.Popover>
<ListBox>
<ListBox.Item>
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</Select.Popover>
</Select>
);Multiple Select
<Select placeholder="Select countries" selectionMode="multiple">
...
</Select>With Disabled Options
<Select disabledKeys={["cat", "kangaroo"]} placeholder="Select an animal">
...
</Select>Custom Indicator
import { ChevronsExpandVertical } from "gravity-icons-solid";
<Select.Indicator class="size-3">
<ChevronsExpandVertical />
</Select.Indicator>With Description
import { Description, Label, ListBox, Select } from "heroui-solid"
import { For } from "solid-js"
const states = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
export function WithDescription() {
return (
<Select class="w-[256px]" placeholder="Select one">
<Label>State</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<For each={states}>
{(state) => (
<ListBox.Item id={state.id} textValue={state.name}>
{state.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Select.Popover>
<Description>Select your state of residence</Description>
</Select>
)
}With Sections
import { Header, Label, ListBox, Select, Separator } from "heroui-solid"
export function WithSections() {
return (
<Select class="w-[256px]" placeholder="Select a country">
<Label>Country</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<ListBox.Section>
<Header>North America</Header>
<ListBox.Item id="usa" textValue="United States">
United States
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="canada" textValue="Canada">
Canada
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="mexico" textValue="Mexico">
Mexico
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox.Section>
<Separator />
<ListBox.Section>
<Header>Europe</Header>
<ListBox.Item id="uk" textValue="United Kingdom">
United Kingdom
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="france" textValue="France">
France
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="germany" textValue="Germany">
Germany
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="spain" textValue="Spain">
Spain
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="italy" textValue="Italy">
Italy
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox.Section>
<Separator />
<ListBox.Section>
<Header>Asia</Header>
<ListBox.Item id="japan" textValue="Japan">
Japan
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="china" textValue="China">
China
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="india" textValue="India">
India
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="south-korea" textValue="South Korea">
South Korea
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox.Section>
</ListBox>
</Select.Popover>
</Select>
)
}Required
import { Button, FieldError, Form, Label, ListBox, Select } from "heroui-solid"
export function Required() {
const onSubmit = (e: SubmitEvent) => {
e.preventDefault()
const formData = new FormData(e.currentTarget as HTMLFormElement)
const data: Record<string, string> = {}
// Convert FormData to plain object
formData.forEach((value, key) => {
data[key] = value.toString()
})
alert("Form submitted successfully!")
}
return (
<Form class="flex w-[256px] flex-col gap-4" onSubmit={onSubmit}>
<Select class="w-full" isRequired name="state" placeholder="Select one">
<Label>State</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<ListBox.Item id="florida" textValue="Florida">
Florida
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="delaware" textValue="Delaware">
Delaware
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="california" textValue="California">
California
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="texas" textValue="Texas">
Texas
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="new-york" textValue="New York">
New York
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="washington" textValue="Washington">
Washington
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</Select.Popover>
<FieldError />
</Select>
<Select
class="w-full"
isRequired
name="country"
placeholder="Select a country"
>
<Label>Country</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<ListBox.Item id="usa" textValue="United States">
United States
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="canada" textValue="Canada">
Canada
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="mexico" textValue="Mexico">
Mexico
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="uk" textValue="United Kingdom">
United Kingdom
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="france" textValue="France">
France
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="germany" textValue="Germany">
Germany
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</Select.Popover>
<FieldError />
</Select>
<Button type="submit">Submit</Button>
</Form>
)
}Controlled
Selected: California
import { Label, ListBox, Select } from "heroui-solid"
import { createSignal, For } from "solid-js"
const states = [
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "florida", name: "Florida" },
{ id: "new-york", name: "New York" },
{ id: "illinois", name: "Illinois" },
{ id: "pennsylvania", name: "Pennsylvania" }
]
export function Controlled() {
const [state, setState] = createSignal<string | null>("california")
const selectedState = () => states.find((s) => s.id === state())
return (
<div style={{ display: "flex", "flex-direction": "column", gap: "0.5rem" }}>
<Select
placeholder="Select a state"
style={{ width: "256px" }}
value={state()}
onChange={(value) => setState(value as string | null)}
>
<Label>State (controlled)</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<For each={states}>
{(s) => (
<ListBox.Item id={s.id} textValue={s.name}>
{s.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Select.Popover>
</Select>
<p style={{ "font-size": "0.875rem", color: "var(--muted)" }}>
Selected: {selectedState()?.name || "None"}
</p>
</div>
)
}Controlled Multiple
Selected: california, texas
import { Label, ListBox, Select } from "heroui-solid"
import { createSignal, For } from "solid-js"
const states = [
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "florida", name: "Florida" },
{ id: "new-york", name: "New York" },
{ id: "illinois", name: "Illinois" },
{ id: "pennsylvania", name: "Pennsylvania" }
]
export function ControlledMultiple() {
const [selected, setSelected] = createSignal<string[]>([
"california",
"texas"
])
return (
<div class="space-y-4">
<Select
class="w-[256px]"
placeholder="Select states"
selectionMode="multiple"
value={selected()}
onChange={(keys) => setSelected(keys as string[])}
>
<Label>States (controlled multiple)</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox selectionMode="multiple">
<For each={states}>
{(s) => (
<ListBox.Item id={s.id} textValue={s.name}>
{s.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Select.Popover>
</Select>
<p class="text-sm text-muted">
Selected: {selected().length > 0 ? selected().join(", ") : "None"}
</p>
</div>
)
}Controlled Open State
Select is closed
import { Button, Label, ListBox, Select } from "heroui-solid"
import { createSignal, For } from "solid-js"
const states = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
export function ControlledOpenState() {
const [isOpen, setIsOpen] = createSignal(false)
return (
<div class="space-y-4">
<Select
class="w-[256px]"
isOpen={isOpen()}
placeholder="Select one"
onOpenChange={setIsOpen}
>
<Label>State</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<For each={states}>
{(state) => (
<ListBox.Item id={state.id} textValue={state.name}>
{state.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Select.Popover>
</Select>
<Button onClick={() => setIsOpen(!isOpen())}>
{isOpen() ? "Close" : "Open"} Select
</Button>
<p class="text-sm text-muted">Select is {isOpen() ? "open" : "closed"}</p>
</div>
)
}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
import { Label, ListBox, Select } from "heroui-solid"
import { For } from "solid-js"
const options = [
{ id: "option1", name: "Option 1" },
{ id: "option2", name: "Option 2" }
]
export function Variants() {
return (
<div style={{ display: "flex", "flex-direction": "column", gap: "1rem" }}>
<Select
placeholder="Select one"
style={{ width: "256px" }}
variant="primary"
>
<Label>Primary variant</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<For each={options}>
{(option) => (
<ListBox.Item id={option.id} textValue={option.name}>
{option.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Select.Popover>
</Select>
<Select
placeholder="Select one"
style={{ width: "256px" }}
variant="secondary"
>
<Label>Secondary variant</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<For each={options}>
{(option) => (
<ListBox.Item id={option.id} textValue={option.name}>
{option.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Select.Popover>
</Select>
</div>
)
}Full Width
<Select fullWidth placeholder="Select one">
...
</Select>In Surface
When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.
import {
Button,
FieldError,
Form,
Label,
ListBox,
Select,
Surface
} from "heroui-solid"
export function OnSurface() {
const onSubmit = (e: SubmitEvent) => {
e.preventDefault()
const formData = new FormData(e.currentTarget as HTMLFormElement)
const data: Record<string, string> = {}
// Convert FormData to plain object
formData.forEach((value, key) => {
data[key] = value.toString()
})
alert("Form submitted successfully!")
}
return (
<Surface class="w-[320px] rounded-3xl p-6">
<Form class="flex w-full flex-col gap-4" onSubmit={onSubmit}>
<Select
class="w-full"
isRequired
name="state"
placeholder="Select one"
variant="secondary"
>
<Label>State</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<ListBox.Item id="florida" textValue="Florida">
Florida
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="delaware" textValue="Delaware">
Delaware
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="california" textValue="California">
California
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="texas" textValue="Texas">
Texas
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="new-york" textValue="New York">
New York
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="washington" textValue="Washington">
Washington
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</Select.Popover>
<FieldError />
</Select>
<Select
class="w-full"
isRequired
name="country"
placeholder="Select a country"
variant="secondary"
>
<Label>Country</Label>
<Select.Trigger>
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<ListBox.Item id="usa" textValue="United States">
United States
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="canada" textValue="Canada">
Canada
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="mexico" textValue="Mexico">
Mexico
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="uk" textValue="United Kingdom">
United Kingdom
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="france" textValue="France">
France
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="germany" textValue="Germany">
Germany
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</Select.Popover>
<FieldError />
</Select>
<Button type="submit">Submit</Button>
</Form>
</Surface>
)
}Custom Value
import {
Avatar,
AvatarFallback,
AvatarImage,
Description,
Label,
ListBox,
Select
} from "heroui-solid"
import { For } from "solid-js"
const users = [
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg",
email: "bob@heroui.com",
fallback: "B",
id: "1",
name: "Bob"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg",
email: "fred@heroui.com",
fallback: "F",
id: "2",
name: "Fred"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg",
email: "martha@heroui.com",
fallback: "M",
id: "3",
name: "Martha"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg",
email: "john@heroui.com",
fallback: "J",
id: "4",
name: "John"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg",
email: "jane@heroui.com",
fallback: "J",
id: "5",
name: "Jane"
}
]
export function CustomValue() {
return (
<Select class="w-[256px]" placeholder="Select a user">
<Label>User</Label>
<Select.Trigger>
<Select.Value>
{(state) => {
const selectedOptions = state.selectedOptions()
if (selectedOptions.length === 0) {
return undefined
}
if (selectedOptions.length > 1) {
return `${selectedOptions.length} users selected`
}
const selectedItem = users.find(
(user) => user.id === selectedOptions[0]?.id
)
if (!selectedItem) {
return undefined
}
return (
<div class="flex items-center gap-2">
<Avatar class="size-4" size="sm">
<AvatarImage src={selectedItem.avatarUrl} />
<AvatarFallback>{selectedItem.fallback}</AvatarFallback>
</Avatar>
<span>{selectedItem.name}</span>
</div>
)
}}
</Select.Value>
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<For each={users}>
{(user) => (
<ListBox.Item id={user.id} textValue={user.name}>
<Avatar size="sm">
<AvatarImage src={user.avatarUrl} />
<AvatarFallback>{user.fallback}</AvatarFallback>
</Avatar>
<div class="flex flex-col">
<Label>{user.name}</Label>
<Description>{user.email}</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Select.Popover>
</Select>
)
}Disabled
<Select isDisabled defaultValue="california" placeholder="Select one">
...
</Select>Related Components
- TextField: Composition-friendly fields with labels and validation
- Label / Description / FieldError: Field satellites, all work inside Select
Styling
Passing Tailwind CSS classes
import { Label, ListBox, Select } from "heroui-solid";
function CustomSelect() {
return (
<Select class="w-full">
<Label>State</Label>
<Select.Trigger class="rounded-lg border bg-surface-secondary p-2">
<Select.Value />
<Select.Indicator />
</Select.Trigger>
<Select.Popover>
<ListBox>
<ListBox.Item id="1" textValue="Item 1" class="hover:bg-surface-secondary">
Item 1
</ListBox.Item>
</ListBox>
</Select.Popover>
</Select>
);
}Customizing the component classes
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;
}
}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
.select- Base select container.select__trigger- The button that triggers the select.select__value- The displayed value or placeholder.select__indicator- The dropdown indicator icon.select__popover- The popover container.list-box/.list-box-item/.list-box-item__indicator- The options list
Variant Classes
.select--primary- Primary variant with shadow (default).select--secondary- Secondary variant without shadow, suitable for use in surfaces
State Classes
.select[data-invalid="true"]- Invalid state.select__trigger[data-focus-visible="true"]- Focused trigger state.select__trigger[data-disabled="true"]- Disabled trigger state
Interactive States
- Hover:
:hoveron trigger - Focus:
:focus-visibleon trigger (shows focus ring) - Disabled:
:disabledon trigger,[data-disabled="true"]on the root - Invalid:
[data-invalid="true"]on the root - Open:
[data-open="true"]on indicator (rotates it) - Placeholder:
[data-placeholder-shown]on the value (Kobalte attribute) - Highlighted item:
[data-highlighted](Kobalte attribute)
API Reference
Select Props
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | JSX.Element | - | 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<string> | - | Keys of disabled items |
isDisabled | boolean | - | Whether the select is disabled |
value | string | string[] | null | - | Current value (controlled) |
defaultValue | string | string[] | null | - | Default value (uncontrolled) |
onChange | (value: string | string[] | null) => void | - | Handler called when the value changes |
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 |
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. |
class | string | - | Additional CSS classes |
children | JSX.Element | - | Select content |
Select.Trigger Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Additional CSS classes |
children | JSX.Element | - | Trigger content |
Select.Value Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Additional CSS classes |
children | JSX.Element | (state: SelectValueState) => JSX.Element | - | Value content or render function |
When using a render function, state exposes selectedOption(), selectedOptions(),
remove(option), and clear() (Kobalte's value state).
Select.Indicator Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Additional CSS classes |
children | JSX.Element | - | Custom indicator content |
Select.Popover Props
| Prop | Type | Default | Description |
|---|---|---|---|
placement | string | "bottom" | Placement relative to the trigger (Kobalte placements, e.g. "top") |
class | string | - | Additional CSS classes |
children | JSX.Element | - | Content children |
ListBox / ListBox.Item Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | Item key (required) |
textValue | string | id | Text used for typeahead and the default value rendering |
isDisabled | boolean | false | Whether the item is disabled |
variant | "default" | "danger" | "default" | Item visual variant |
class | string | - | Additional CSS classes |
children | JSX.Element | - | Item content |
Accessibility
The Select component implements the ARIA listbox pattern (via Kobalte) 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
Differences from HeroUI React
- Built on Kobalte's Select, which is data-driven: this port bridges the JSX item API
by collecting
ListBox.Itemdescriptors and registering them as Kobalte options. Inside aSelect, the ListBox selection props are ignored — the Select root owns the selection state. ListBox.Sectionis not supported inside aSelectyet (the popover marker protocol can't defer the section header) — flat item lists only.Select.Valuerender props expose Kobalte's state (selectedOptions()accessors) instead of React Aria's{ defaultChildren, isPlaceholder, state }.Select.Popoverplacements use Kobalte's syntax ("top","bottom-start", …) instead of React Aria's ("top left").- Popover enter/exit animations ride Kobalte's
data-expanded/data-closedattributes; the placeholder state is[data-placeholder-shown]. - No
renderprop — use Kobalte's polymorphicaswhere supported.
Last updated: 7/19/26, 3:27 AM