Listbox
Basic example
Listboxes are built using a combination of the Listbox
, ListboxButton
, ListboxOptions
, ListboxOption
, and ListboxLabel
components.
The ListboxButton
will automatically open/close the ListboxOptions
when clicked, and when the listbox is open, the list of items receives focus and is automatically navigable via the keyboard.
<script>
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
} from "@rgossiaux/svelte-headlessui";
const people = [
{ id: 1, name: "Durward Reynolds", unavailable: false },
{ id: 2, name: "Kenton Towne", unavailable: false },
{ id: 3, name: "Therese Wunsch", unavailable: false },
{ id: 4, name: "Benedict Kessler", unavailable: true },
{ id: 5, name: "Katelyn Rohan", unavailable: false },
];
let selectedPerson = people[0];
</script>
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions>
{#each people as person (person.id)}
<ListboxOption value={person} disabled={person.unavailable}>
{person.name}
</ListboxOption>
{/each}
</ListboxOptions>
</Listbox>
Styling
See here for some general notes on styling the components in this library.
Active and selected items
To style the active ListboxOption
, you can use the active
slot prop that it provides, which tells you whether or not that option is currently focused via the mouse or keyboard.
To style the selected ListboxOption
, you can use the selected
slot prop that it provides, which tells you whether or not that option is the selected option (the one passed to the value
prop of the <Listbox>
)
You can use these states to conditionally apply whatever active/focus styles you wish.
<script>
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
} from "@rgossiaux/svelte-headlessui";
import { CheckIcon } from "@rgossiaux/svelte-heroicons/solid";
const people = [
{ id: 1, name: "Durward Reynolds" },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];
let selectedPerson = people[0];
</script>
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions>
{#each people as person (person.id)}
<!-- Use the `active` state to conditionally style the active (focused) option -->
<!-- Use the `selected` state to conditionally style the selected option -->
<ListboxOption
value={person}
disabled={person.unavailable}
class={({ active }) => (active ? "active" : "")}
let:selected
>
{#if selected}
<CheckIcon />
{/if}
{person.name}
</ListboxOption>
{/each}
</ListboxOptions>
</Listbox>
Using a custom label
By default, the Listbox
will use the <ListboxButton>
contents as the label for screenreaders. If you’d like more control over what is announced to assistive technologies, use the ListboxLabel
component:
<script>
import {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOptions,
ListboxOption,
} from "@rgossiaux/svelte-headlessui";
const people = [
{ id: 1, name: "Durward Reynolds" },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];
let selectedPerson = people[0];
</script>
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
<ListboxLabel>Assignee:</ListboxLabel>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions>
{#each people as person (person.id)}
<ListboxOption value={person}>
{person.name}
</ListboxOption>
{/each}
</ListboxOptions>
</Listbox>
Showing/hiding the listbox
By default, the ListboxOptions
instance will be shown and hidden automatically based on the internal open
state tracked by the Listbox
component itself.
If you’d rather handle this yourself (perhaps because you need to add an extra wrapper element for one reason or another), you can add a static
prop to the ListboxOptions
component to tell it to always render, and use the open
slot prop provided by the Listbox
to show or hide the listbox yourself.
<script>
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
} from "@rgossiaux/svelte-headlessui";
const people = [
{ id: 1, name: "Durward Reynolds" },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];
let selectedPerson = people[0];
</script>
<Listbox
value={selectedPerson}
on:change={(e) => (selectedPerson = e.detail)}
let:open
>
<ListboxButton>{selectedPerson.name}</ListboxButton>
{#if open}
<div>
<!-- Using `static`, the `ListboxOptions` is always rendered
and ignores the internal `open` state -->
<ListboxOptions static>
{#each people as person (person.id)}
<ListboxOption value={person}>
{person.name}
</ListboxOption>
{/each}
</ListboxOptions>
</div>
{/if}
</Listbox>
You can also choose to have the Listbox
merely hide the ListboxOptions
when the Listbox
is closed, instead of removing it from the DOM entirely, by using the unmount
prop. This may be useful for performance reasons if rendering the ListboxOptions
is expensive.
<script>
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
} from "@rgossiaux/svelte-headlessui";
const people = [
{ id: 1, name: "Durward Reynolds" },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];
let selectedPerson = people[0];
</script>
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<!-- Using `unmount={false}`, the `ListboxOptions` is kept in the DOM
when the `Listbox` is closed -->
<ListboxOptions unmount={false}>
{#each people as person (person.id)}
<ListboxOption value={person}>
{person.name}
</ListboxOption>
{/each}
</ListboxOptions>
</Listbox>
Disabling an item
Use the disabled
prop to disable a ListboxOption
. This will make it unselectable via mouse and keyboard navigation, and it will be skipped when pressing the up/down arrows.
<script>
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
} from "@rgossiaux/svelte-headlessui";
const people = [
{ id: 1, name: "Durward Reynolds", unavailable: false },
{ id: 2, name: "Kenton Towne", unavailable: false },
{ id: 3, name: "Therese Wunsch", unavailable: false },
{ id: 4, name: "Benedict Kessler", unavailable: true },
{ id: 5, name: "Katelyn Rohan", unavailable: false },
];
let selectedPerson = people[0];
</script>
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions>
{#each people as person (person.id)}
<ListboxOption value={person} disabled={person.unavailable}>
<span class:unavailable={person.unavailable}>
{person.name}
</span>
</ListboxOption>
{/each}
</ListboxOptions>
</Listbox>
Transitions
To animate the opening and closing of the listbox, you can use this library’s Transition component or Svelte’s built-in transition engine. See that page for a comparison.
Using the Transition
component
To use the Transition
component, all you need to do is wrap the ListboxOptions
in a <Transition>
, and the transition will be applied automatically.
<script>
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
Transition,
} from "@rgossiaux/svelte-headlessui";
const people = [
{ id: 1, name: "Durward Reynolds" },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];
let selectedPerson = people[0];
</script>
<Listbox value={selectedPerson} on:change={(e) => (selectedPerson = e.detail)}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<!-- Example using Tailwind CSS transition classes -->
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<ListboxOptions>
{#each people as person (person.id)}
<ListboxOption value={person}>
{person.name}
</ListboxOption>
{/each}
</ListboxOptions>
</Transition>
</Listbox>
The components in this library communicate with each other, so the Transition will be managed automatically when the Listbox is opened/closed. If you require more control over this behavior, you may use a more explicit version:
<script>
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
Transition,
} from "@rgossiaux/svelte-headlessui";
const people = [
{ id: 1, name: "Durward Reynolds" },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];
let selectedPerson = people[0];
</script>
<Listbox
value={selectedPerson}
on:change={(e) => (selectedPerson = e.detail)}
let:open
>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<!-- Example using Tailwind CSS transition classes -->
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<!-- When controlling the transition manually, make sure to use `static` -->
<ListboxOptions static>
{#each people as person (person.id)}
<ListboxOption value={person}>
{person.name}
</ListboxOption>
{/each}
</ListboxOptions>
</Transition>
</Listbox>
Using Svelte transitions
The last example above also provides a blueprint for using Svelte transitions:
<script>
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
} from "@rgossiaux/svelte-headlessui";
import { fade } from "svelte/transition";
const people = [
{ id: 1, name: "Durward Reynolds" },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];
let selectedPerson = people[0];
</script>
<Listbox
value={selectedPerson}
on:change={(e) => (selectedPerson = e.detail)}
let:open
>
<ListboxButton>{selectedPerson.name}</ListboxButton>
{#if open}
<div transition:fade>
<!-- When controlling the transition manually, make sure to use `static` -->
<ListboxOptions static>
{#each people as person (person.id)}
<ListboxOption value={person}>
{person.name}
</ListboxOption>
{/each}
</ListboxOptions>
</div>
{/if}
</Listbox>
Make sure to use the static
prop, or else the exit transitions won’t work correctly.
Horizontal options
If you’ve styled your ListboxOptions
to appear horizontally, use the horizontal
prop on the Listbox
component to enable navigating the items with the left and right arrow keys instead of up and down, and to update the aria-orientation
attribute for assistive technologies.
<script>
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
} from "@rgossiaux/svelte-headlessui";
const people = [
{ id: 1, name: "Durward Reynolds" },
{ id: 2, name: "Kenton Towne" },
{ id: 3, name: "Therese Wunsch" },
{ id: 4, name: "Benedict Kessler" },
{ id: 5, name: "Katelyn Rohan" },
];
let selectedPerson = people[0];
</script>
<Listbox
value={selectedPerson}
on:change={(e) => (selectedPerson = e.detail)}
horizontal
>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions>
{#each people as person (person.id)}
<ListboxOption value={person}>
{person.name}
</ListboxOption>
{/each}
</ListboxOptions>
</Listbox>
Accessibility notes
Focus management
When a Listbox is toggled open, the ListboxOptions
receives focus. Focus is trapped within the list of items until Escape is pressed or the user clicks outside the options. Closing the Listbox
returns focus to the ListboxButton
.
Mouse interaction
Clicking a ListboxButton
toggles the options list open and closed. Clicking anywhere outside of the options list will close the listbox.
Keyboard interaction
When the horizontal
prop is set, the <ArrowUp>
and <ArrowDown>
below become <ArrowLeft>
and <ArrowRight>
:
Command | Description |
---|---|
<Enter> / <Space> / <ArrowUp> / <ArrowDown> when ListboxButton is focused |
Opens listbox and focuses the selected item |
<Esc> when listbox is open |
Closes listbox |
<ArrowDown> / <ArrowUp> when listbox is open |
Focuses next/previous non-disabled option |
<Home> / <End> when listbox is open |
Focuses first/last non-disabled option |
<Enter> / <Space> when listbox is open |
Selects the focused option |
<A-Za-z> when listbox is open |
Focuses next option that matches keyboard input |
Other
All relevant ARIA attributes are automatically managed.
For a full reference on all accessibility features implemented in Listbox
, see the ARIA spec on Listbox.
Component API
Listbox
The main listbox component.
Prop | Default | Type | Description |
---|---|---|---|
as |
div |
string |
The element the Listbox should render as |
disabled |
false |
boolean |
Whether the entire Listbox and its children should be disabled |
horizontal |
false |
boolean |
Whether the entire Listbox should be oriented horizontally instead of vertically |
value |
— | T |
The selected value |
Slot prop | Type | Description |
---|---|---|
disabled |
boolean |
Whether or not the listbox is disabled |
open |
boolean |
Whether or not the listbox is open |
This component also dispatches a custom event, which is listened to using the Svelte on:
directive:
Event name | Type of event .detail |
Description |
---|---|---|
change |
T |
Dispatched when a ListboxOption is selected; the event detail contains the value of the selected option |
ListboxButton
The listbox’s button.
Prop | Default | Type | Description |
---|---|---|---|
as |
button |
string |
The element the ListboxButton should render as |
Slot prop | Type | Description |
---|---|---|
disabled |
boolean |
Whether or not the listbox is disabled |
open |
boolean |
Whether or not the listbox is open |
ListboxLabel
A label that can be used for more control over the text your listbox will announce to screenreaders. Renders an element that is linked to the root Listbox
via the aria-labelledby
attribute and an autogenerated id.
Prop | Default | Type | Description |
---|---|---|---|
as |
label |
string |
The element the ListboxLabel should render as |
Slot prop | Type | Description |
---|---|---|
disabled |
boolean |
Whether or not the listbox is disabled |
open |
boolean |
Whether or not the listbox is open |
ListboxOptions
The component that directly wraps the list of options in your custom listbox.
Prop | Default | Type | Description |
---|---|---|---|
as |
ul |
string |
The element the ListboxOptions should render as |
static |
false |
boolean |
Whether the element should ignore the internally managed open/closed state |
unmount |
true |
boolean |
Whether the element should be unmounted, instead of just hidden, based on the open/closed state |
Note that static
and unmount
cannot be used together.
Slot prop | Type | Description |
---|---|---|
open |
boolean |
Whether or not the listbox is open |
ListboxOption
Used to wrap each item within your listbox.
Prop | Default | Type | Description |
---|---|---|---|
value |
— | T |
The option value |
as |
li |
string |
The element the ListboxOption should render as |
disabled |
false |
boolean |
Whether the option should be disabled for keyboard navigation and ARIA purposes |
Slot prop | Type | Description |
---|---|---|
active |
boolean |
Whether the option is active (focused) |
selected |
boolean |
Whether the option is selected |
disabled |
boolean |
Whether the option is disabled |