A listbox displays a list of options and allows a user to select one or more of them
Import
import { ListBox } from "heroui-solid";Usage
- Bbob@heroui.com
- Ffred@heroui.com
- Mmartha@heroui.com
import { Avatar, Description, Label, ListBox } from "heroui-solid"
export function Default() {
return (
<ListBox aria-label="Users" class="w-[220px]" selectionMode="single">
<ListBox.Item id="1" textValue="Bob">
<Avatar size="sm">
<Avatar.Image
alt="Bob"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg"
/>
<Avatar.Fallback>B</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Bob</Label>
<Description>bob@heroui.com</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="2" textValue="Fred">
<Avatar size="sm">
<Avatar.Image
alt="Fred"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg"
/>
<Avatar.Fallback>F</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Fred</Label>
<Description>fred@heroui.com</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="3" textValue="Martha">
<Avatar size="sm">
<Avatar.Image
alt="Martha"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg"
/>
<Avatar.Fallback>M</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Martha</Label>
<Description>martha@heroui.com</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
)
}Anatomy
Import the ListBox component and access all parts using dot notation.
import { ListBox, Label, Description, Header } from "heroui-solid";
export default () => (
<ListBox>
<ListBox.Item>
<Label />
<Description />
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Section>
<Header />
<ListBox.Item>
<Label />
</ListBox.Item>
</ListBox.Section>
</ListBox>
);With Sections
Actions - ⌘NCreate a new file
- ⌘EMake changes
Danger zone - ⌘⇧DMove to trash
import { Pencil, SquarePlus, TrashBin } from "gravity-icons-solid"
import {
Description,
Header,
Kbd,
Label,
ListBox,
Separator,
Surface
} from "heroui-solid"
export function WithSections() {
return (
<Surface class="w-[256px] rounded-3xl shadow-surface">
<ListBox
aria-label="File actions"
class="w-full p-2"
selectionMode="none"
onAction={(key) => alert(`Selected item: ${key}`)}
>
<ListBox.Section>
<Header>Actions</Header>
<ListBox.Item id="new-file" textValue="New file">
<div class="flex h-8 items-start justify-center pt-px">
<SquarePlus class="size-4 shrink-0 text-muted" />
</div>
<div class="flex flex-col">
<Label>New file</Label>
<Description>Create a new file</Description>
</div>
<Kbd class="ms-auto" variant="light">
<Kbd.Abbr keyValue="command" />
<Kbd.Content>N</Kbd.Content>
</Kbd>
</ListBox.Item>
<ListBox.Item id="edit-file" textValue="Edit file">
<div class="flex h-8 items-start justify-center pt-px">
<Pencil class="size-4 shrink-0 text-muted" />
</div>
<div class="flex flex-col">
<Label>Edit file</Label>
<Description>Make changes</Description>
</div>
<Kbd class="ms-auto" variant="light">
<Kbd.Abbr keyValue="command" />
<Kbd.Content>E</Kbd.Content>
</Kbd>
</ListBox.Item>
</ListBox.Section>
<Separator />
<ListBox.Section>
<Header>Danger zone</Header>
<ListBox.Item
id="delete-file"
textValue="Delete file"
variant="danger"
>
<div class="flex h-8 items-start justify-center pt-px">
<TrashBin class="size-4 shrink-0 text-danger" />
</div>
<div class="flex flex-col">
<Label>Delete file</Label>
<Description>Move to trash</Description>
</div>
<Kbd class="ms-auto" variant="light">
<Kbd.Abbr keyValue="command" />
<Kbd.Abbr keyValue="shift" />
<Kbd.Content>D</Kbd.Content>
</Kbd>
</ListBox.Item>
</ListBox.Section>
</ListBox>
</Surface>
)
}Multi Select
- Bbob@heroui.com
- Ffred@heroui.com
- Mmartha@heroui.com
import { Avatar, Description, Label, ListBox, Surface } from "heroui-solid"
export function MultiSelect() {
return (
<Surface class="w-[256px] rounded-3xl shadow-surface">
<ListBox aria-label="Users" selectionMode="multiple">
<ListBox.Item id="1" textValue="Bob">
<Avatar size="sm">
<Avatar.Image
alt="Bob"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg"
/>
<Avatar.Fallback>B</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Bob</Label>
<Description>bob@heroui.com</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="2" textValue="Fred">
<Avatar size="sm">
<Avatar.Image
alt="Fred"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg"
/>
<Avatar.Fallback>F</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Fred</Label>
<Description>fred@heroui.com</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="3" textValue="Martha">
<Avatar size="sm">
<Avatar.Image
alt="Martha"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg"
/>
<Avatar.Fallback>M</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Martha</Label>
<Description>martha@heroui.com</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</Surface>
)
}With Disabled Items
Actions - ⌘NCreate a new file
- ⌘EMake changes
Danger zone - ⌘⇧DMove to trash
import { Pencil, SquarePlus, TrashBin } from "gravity-icons-solid"
import {
Description,
Header,
Kbd,
Label,
ListBox,
Separator,
Surface
} from "heroui-solid"
export function WithDisabledItems() {
return (
<Surface class="w-[256px] rounded-3xl shadow-surface">
<ListBox
aria-label="File actions"
class="w-full p-2"
disabledKeys={["delete-file"]}
selectionMode="none"
onAction={(key) => alert(`Selected item: ${key}`)}
>
<ListBox.Section>
<Header>Actions</Header>
<ListBox.Item id="new-file" textValue="New file">
<div class="flex h-8 items-start justify-center pt-px">
<SquarePlus class="size-4 shrink-0 text-muted" />
</div>
<div class="flex flex-col">
<Label>New file</Label>
<Description>Create a new file</Description>
</div>
<Kbd class="ms-auto" variant="light">
<Kbd.Abbr keyValue="command" />
<Kbd.Content>N</Kbd.Content>
</Kbd>
</ListBox.Item>
<ListBox.Item id="edit-file" textValue="Edit file">
<div class="flex h-8 items-start justify-center pt-px">
<Pencil class="size-4 shrink-0 text-muted" />
</div>
<div class="flex flex-col">
<Label>Edit file</Label>
<Description>Make changes</Description>
</div>
<Kbd class="ms-auto" variant="light">
<Kbd.Abbr keyValue="command" />
<Kbd.Content>E</Kbd.Content>
</Kbd>
</ListBox.Item>
</ListBox.Section>
<Separator />
<ListBox.Section>
<Header>Danger zone</Header>
<ListBox.Item
id="delete-file"
textValue="Delete file"
variant="danger"
>
<div class="flex h-8 items-start justify-center pt-px">
<TrashBin class="size-4 shrink-0 text-danger" />
</div>
<div class="flex flex-col">
<Label>Delete file</Label>
<Description>Move to trash</Description>
</div>
<Kbd class="ms-auto" variant="light">
<Kbd.Abbr keyValue="command" />
<Kbd.Abbr keyValue="shift" />
<Kbd.Content>D</Kbd.Content>
</Kbd>
</ListBox.Item>
</ListBox.Section>
</ListBox>
</Surface>
)
}Custom Check Icon
- Bbob@heroui.com
- Ffred@heroui.com
- Mmartha@heroui.com
import { Check } from "gravity-icons-solid"
import { Avatar, Description, Label, ListBox, Surface } from "heroui-solid"
export function CustomCheckIcon() {
return (
<Surface class="w-[256px] rounded-3xl shadow-surface">
<ListBox aria-label="Users" selectionMode="multiple">
<ListBox.Item id="1" textValue="Bob">
<Avatar size="sm">
<Avatar.Image
alt="Bob"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg"
/>
<Avatar.Fallback>B</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Bob</Label>
<Description>bob@heroui.com</Description>
</div>
<ListBox.ItemIndicator>
<Check class="size-4 text-accent-soft-foreground" />
</ListBox.ItemIndicator>
</ListBox.Item>
<ListBox.Item id="2" textValue="Fred">
<Avatar size="sm">
<Avatar.Image
alt="Fred"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg"
/>
<Avatar.Fallback>F</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Fred</Label>
<Description>fred@heroui.com</Description>
</div>
<ListBox.ItemIndicator>
<Check class="size-4 text-accent-soft-foreground" />
</ListBox.ItemIndicator>
</ListBox.Item>
<ListBox.Item id="3" textValue="Martha">
<Avatar size="sm">
<Avatar.Image
alt="Martha"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg"
/>
<Avatar.Fallback>M</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Martha</Label>
<Description>martha@heroui.com</Description>
</div>
<ListBox.ItemIndicator>
<Check class="size-4 text-accent-soft-foreground" />
</ListBox.ItemIndicator>
</ListBox.Item>
</ListBox>
</Surface>
)
}Controlled
- Bbob@heroui.com
- Ffred@heroui.com
- Mmartha@heroui.com
Selected: 1
import { Check } from "gravity-icons-solid"
import { Avatar, Description, Label, ListBox, Surface } from "heroui-solid"
import { createSignal } from "solid-js"
export function Controlled() {
const [selected, setSelected] = createSignal<Set<string>>(new Set(["1"]))
const selectedItems = () => Array.from(selected())
return (
<div class="space-y-4">
<Surface class="w-[256px] rounded-3xl shadow-surface">
<ListBox
aria-label="Users"
selectedKeys={selected()}
selectionMode="multiple"
onSelectionChange={setSelected}
>
<ListBox.Item id="1" textValue="Bob">
<Avatar size="sm">
<Avatar.Image
alt="Bob"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg"
/>
<Avatar.Fallback>B</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Bob</Label>
<Description>bob@heroui.com</Description>
</div>
<ListBox.ItemIndicator>
<Check class="size-4 text-accent-soft-foreground" />
</ListBox.ItemIndicator>
</ListBox.Item>
<ListBox.Item id="2" textValue="Fred">
<Avatar size="sm">
<Avatar.Image
alt="Fred"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg"
/>
<Avatar.Fallback>F</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Fred</Label>
<Description>fred@heroui.com</Description>
</div>
<ListBox.ItemIndicator>
<Check class="size-4 text-accent-soft-foreground" />
</ListBox.ItemIndicator>
</ListBox.Item>
<ListBox.Item id="3" textValue="Martha">
<Avatar size="sm">
<Avatar.Image
alt="Martha"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg"
/>
<Avatar.Fallback>M</Avatar.Fallback>
</Avatar>
<div class="flex flex-col">
<Label>Martha</Label>
<Description>martha@heroui.com</Description>
</div>
<ListBox.ItemIndicator>
<Check class="size-4 text-accent-soft-foreground" />
</ListBox.ItemIndicator>
</ListBox.Item>
</ListBox>
</Surface>
<p class="text-sm text-muted">
Selected:{" "}
{selectedItems().length > 0 ? selectedItems().join(", ") : "None"}
</p>
</div>
)
}Scrollbar Modes
HeroUI thin
Browser default
Hidden
import { ListBox, Surface } from "heroui-solid"
import { For } from "solid-js"
type ScrollbarMode = {
id: string
label: string
scrollbar?: "thin" | "default" | "none"
}
const modes: ScrollbarMode[] = [
{
id: "heroui",
label: "HeroUI thin",
scrollbar: "thin"
},
{
id: "browser",
label: "Browser default",
scrollbar: "default"
},
{
id: "hidden",
label: "Hidden",
scrollbar: "none"
}
]
const animals = [
{ id: "aardvark", name: "Aardvark" },
{ id: "alpaca", name: "Alpaca" },
{ id: "antelope", name: "Antelope" },
{ id: "bear", name: "Bear" },
{ id: "cat", name: "Cat" },
{ id: "dog", name: "Dog" },
{ id: "fox", name: "Fox" },
{ id: "giraffe", name: "Giraffe" },
{ id: "kangaroo", name: "Kangaroo" },
{ id: "koala", name: "Koala" },
{ id: "lemur", name: "Lemur" },
{ id: "otter", name: "Otter" },
{ id: "panda", name: "Panda" },
{ id: "penguin", name: "Penguin" },
{ id: "rabbit", name: "Rabbit" },
{ id: "snake", name: "Snake" },
{ id: "turtle", name: "Turtle" },
{ id: "wombat", name: "Wombat" },
{ id: "zebra", name: "Zebra" }
]
function ScrollbarListBox(props: { mode: ScrollbarMode }) {
return (
<div class="flex w-[260px] flex-col gap-2">
<h3 class="px-1 text-sm font-semibold text-muted">{props.mode.label}</h3>
<Surface
class="overflow-hidden rounded-3xl shadow-surface"
data-scrollbar={props.mode.scrollbar}
>
<div class="h-52 scrollbar overflow-y-auto p-1">
<ListBox
aria-label={`${props.mode.label} animals`}
selectionMode="single"
>
<For each={animals}>
{(animal) => (
<ListBox.Item
class="text-sm leading-5 font-medium"
id={`${props.mode.id}-${animal.id}`}
textValue={animal.name}
>
{animal.name}
</ListBox.Item>
)}
</For>
</ListBox>
</div>
</Surface>
</div>
)
}
export function ScrollbarModes() {
return (
<div class="flex w-full flex-wrap justify-center gap-4">
<For each={modes}>{(mode) => <ScrollbarListBox mode={mode} />}</For>
</div>
)
}Virtualization
ListBox supports virtualization through Virtualizer, enabling efficient rendering of large datasets by displaying only the rows visible within the viewport. It wraps the ListBox and windows the collection with @tanstack/solid-virtual over Kobalte's virtualized listbox.
- emma.smith@acme.com
- liam.smith@acme.com
- olivia.smith@acme.com
- noah.smith@acme.com
- ava.smith@acme.com
- james.smith@acme.com
- sophia.smith@acme.com
- oliver.smith@acme.com
- isabella.smith@acme.com
- lucas.smith@acme.com
- mia.smith@acme.com
- ethan.smith@acme.com
- charlotte.smith@acme.com
- mason.smith@acme.com
- amelia.smith@acme.com
- logan.smith@acme.com
- harper.smith@acme.com
import {
Description,
Label,
ListBox,
ListLayout,
Virtualizer
} from "heroui-solid"
interface User {
id: number
name: string
email: string
}
export function Virtualization() {
const firstNames = [
"Emma",
"Liam",
"Olivia",
"Noah",
"Ava",
"James",
"Sophia",
"Oliver",
"Isabella",
"Lucas",
"Mia",
"Ethan",
"Charlotte",
"Mason",
"Amelia",
"Logan",
"Harper",
"Alexander",
"Ella",
"Benjamin"
]
const lastNames = [
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Garcia",
"Miller",
"Davis",
"Rodriguez",
"Martinez",
"Anderson",
"Taylor",
"Thomas",
"Jackson",
"White",
"Harris",
"Clark",
"Lewis",
"Robinson",
"Walker"
]
function generateUsers(n: number): User[] {
const users: User[] = []
for (let i = 0; i < n; i++) {
const firstName = firstNames[i % firstNames.length]
const lastName =
lastNames[Math.floor(i / firstNames.length) % lastNames.length]
const name = `${firstName} ${lastName}`
users.push({
email: `${firstName?.toLowerCase()}.${lastName?.toLowerCase()}@acme.com`,
id: i + 1,
name
})
}
return users
}
const users = generateUsers(1000)
return (
<Virtualizer layout={ListLayout} layoutOptions={{ rowHeight: 50 }}>
<ListBox
aria-label="Virtualized list with 1000 items"
class="h-[400px] w-[300px] overflow-y-auto"
items={users}
>
{(user: User) => (
<ListBox.Item id={String(user.id)} textValue={user.name}>
<div class="flex flex-col">
<Label>{user.name}</Label>
<Description>{user.email}</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</ListBox>
</Virtualizer>
)
}Styling
Passing Tailwind CSS classes
import { ListBox } from "heroui-solid";
function CustomListBox() {
return (
<ListBox class="border rounded-lg p-2 bg-surface">
<ListBox.Item id="1" textValue="Item 1" class="hover:bg-surface-secondary">
Item 1
</ListBox.Item>
</ListBox>
);
}Customizing the component classes
To customize the ListBox component classes, you can use the @layer components directive.
Learn more.
@layer components {
.list-box {
@apply rounded-lg border border-border bg-surface p-2;
}
.list-box-item {
@apply rounded px-2 py-1 cursor-pointer;
}
.list-box-item--danger {
@apply text-danger;
}
.list-box-item__indicator {
@apply text-accent;
}
}HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
CSS Classes
The ListBox component uses these CSS classes (View source styles):
Base Classes
.list-box- Base listbox container.list-box-item- Individual listbox item.list-box-item__indicator- Selection indicator icon.list-box-section- Section container for grouping items
Variant Classes
.list-box--default- Default variant styling.list-box--danger- Danger variant styling.list-box-item--default- Default item variant.list-box-item--danger- Danger item variant
State Classes
.list-box-item[aria-selected="true"]- Selected item state.list-box-item:focus-visible- Focused item state.list-box-item[data-disabled]- Disabled item state (Kobalte attribute)
Interactive States
The component supports both CSS pseudo-classes and data attributes for flexibility:
- Hover / keyboard highlight:
:hoveror[data-highlighted]on item (Kobalte attribute) - Focus:
:focus-visibleon item - Selected:
[aria-selected="true"]on item - Disabled:
[data-disabled]on item (Kobalte attribute)
API Reference
ListBox Props
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | - | Accessibility label for the listbox |
aria-labelledby | string | - | ID of element that labels the listbox |
selectionMode | "none" | "single" | "multiple" | "single" | Selection behavior |
selectedKeys | Iterable<string> | - | Controlled selected keys |
defaultSelectedKeys | Iterable<string> | - | Initial selected keys |
onSelectionChange | (keys: Set<string>) => void | - | Handler called when selection changes |
disabledKeys | Iterable<string> | - | Keys of disabled items |
onAction | (key: string) => void | - | Handler called when an item is activated |
variant | "default" | "danger" | "default" | Visual variant |
class | string | - | Additional CSS classes |
children | JSX.Element | - | ListBox items and sections |
Inside a Select, the selection props are ignored — the enclosing Select root owns the selection state.
ListBox.Item Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique identifier for the item |
textValue | string | id | Text value for accessibility and typeahead |
isDisabled | boolean | false | Whether this item is disabled |
variant | "default" | "danger" | "default" | Visual variant |
class | string | - | Additional CSS classes |
children | JSX.Element | - | Item content |
ListBox.ItemIndicator Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Additional CSS classes |
children | JSX.Element | - | Custom indicator content (defaults to the checkmark) |
forceMount | boolean | - | Render the indicator even while unselected |
ListBox.Section Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Additional CSS classes |
children | JSX.Element | - | Section content including Header and Items |
Examples
Basic Usage
import { ListBox, Label, Description } from "heroui-solid";
<ListBox aria-label="Users" selectionMode="single">
<ListBox.Item id="1" textValue="Bob">
<Label>Bob</Label>
<Description>bob@heroui.com</Description>
</ListBox.Item>
<ListBox.Item id="2" textValue="Alice">
<Label>Alice</Label>
<Description>alice@heroui.com</Description>
</ListBox.Item>
</ListBox>With Sections
import { ListBox, Header, Separator } from "heroui-solid";
<ListBox aria-label="Actions" selectionMode="none" onAction={(key) => console.log(key)}>
<ListBox.Section>
<Header>Actions</Header>
<ListBox.Item id="new" textValue="New file">New file</ListBox.Item>
<ListBox.Item id="edit" textValue="Edit file">Edit file</ListBox.Item>
</ListBox.Section>
<Separator />
<ListBox.Section>
<Header>Danger zone</Header>
<ListBox.Item id="delete" textValue="Delete" variant="danger">Delete</ListBox.Item>
</ListBox.Section>
</ListBox>Controlled Selection
import { ListBox } from "heroui-solid";
import { createSignal } from "solid-js";
function ControlledListBox() {
const [selected, setSelected] = createSignal<Set<string>>(new Set(["1"]));
return (
<ListBox
aria-label="Options"
selectedKeys={selected()}
selectionMode="multiple"
onSelectionChange={setSelected}
>
<ListBox.Item id="1" textValue="Option 1">Option 1</ListBox.Item>
<ListBox.Item id="2" textValue="Option 2">Option 2</ListBox.Item>
<ListBox.Item id="3" textValue="Option 3">Option 3</ListBox.Item>
</ListBox>
);
}Custom Indicator
import { Check } from "gravity-icons-solid";
import { ListBox } from "heroui-solid";
<ListBox aria-label="Options" selectionMode="multiple">
<ListBox.Item id="1" textValue="Option 1">
Option 1
<ListBox.ItemIndicator>
<Check class="size-4" />
</ListBox.ItemIndicator>
</ListBox.Item>
</ListBox>Accessibility
The ListBox component implements the ARIA listbox pattern and provides:
- Full keyboard navigation support
- Proper focus management
- Support for disabled states
- Typeahead search functionality
Differences from HeroUI React
- Built on Kobalte's Listbox, which is data-driven: this port bridges the JSX item API
by collecting
ListBox.ItemandListBox.Sectiondescriptors and passing them as Kobalte options. - Sections render flat: Kobalte's collection has no group wrapper, so
ListBox.Sectionrenders as a presentational label between sibling options instead of React Aria'srole="group"with an accessible name. - Sections are not supported inside a
Selectyet (its popover marker protocol can't defer the section header). ListBox.ItemIndicatortakes plain children instead of a render function — it only mounts while its item is selected (passforceMountto keep it in the DOM).- Virtualization (
Virtualizer/ListLayout) is not ported. - Selection is reported as Kobalte's
Set<string>instead of React Aria'sSelection("all" | Set<Key>), and keys are strings. - The React
renderprop is replaced by Kobalte'sasprop.
Last updated: 7/19/26, 3:27 AM