Svelte Headless UI

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>

Using with HTML forms

If you add the name prop to your listbox, hidden input elements will be rendered and kept in sync with your selected value.

<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)} name="assignee">
  <ListboxButton>{selectedPerson.name}</ListboxButton>
  <ListboxOptions>
    {#each people as person (person.id)}
      <ListboxOption value={person} disabled={person.unavailable}>
        {person.name}
      </ListboxOption>
    {/each}
  </ListboxOptions>
</Listbox>

This lets you use a listbox inside a native HTML <form> and make traditional form submissions as if your listbox was a native HTML form control.

Basic values like strings will be rendered as a single hidden input containing that value, but complex values like objects will be encoded into multiple inputs using a square bracket notation for the names:

<input type="hidden" name="assignee[id]" value="1" />
<input type="hidden" name="assignee[name]" value="Durward Reynolds" />

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
name string The name used when using this component inside a form.
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