Listbox

The listbox component provides a way to select one or more options from a list of options.

WAI-ARIA: Listbox Pattern

Features

Multiple

The listbox supports a multiple option with enhanced keyboard navigation.

Keyboard navigation

  • Space on an option selects it.
  • Arrow Up or Arrow Down navigates between options.
  • CTRL + A selects or unselects all the options if multiple.

Example

You have selected: ""

Cat
Dog
Rabbit
Mouse
Rat
Bird

Same example but multiple:

Cat
Dog
Rabbit
Mouse
Rat
Bird

Sources

+page.svelte

<script lang="ts">
  import Listbox from './Listbox.svelte'
  import ListboxItem from './ListboxItem.svelte'

  let value: string | string[] = ''
</script>

<p class="mb-4 text-sm opacity-60">
  You have selected: <strong>{JSON.stringify(value)}</strong>
</p>

<Listbox bind:value label="Select your favourite pet">
  <ListboxItem value="🐈">Cat</ListboxItem>
  <ListboxItem value="🐕">Dog</ListboxItem>
  <ListboxItem value="🐇">Rabbit</ListboxItem>
  <ListboxItem value="🐁">Mouse</ListboxItem>
  <ListboxItem value="🐀">Rat</ListboxItem>
  <ListboxItem value="🐦">Bird</ListboxItem>
</Listbox>

<br />

<p class="mb-4 text-sm opacity-60">Same example but multiple:</p>

<Listbox label="Select your favourite pets" multiple>
  <ListboxItem value="🐈">Cat</ListboxItem>
  <ListboxItem value="🐕">Dog</ListboxItem>
  <ListboxItem value="🐇">Rabbit</ListboxItem>
  <ListboxItem value="🐁">Mouse</ListboxItem>
  <ListboxItem value="🐀">Rat</ListboxItem>
  <ListboxItem value="🐦">Bird</ListboxItem>
</Listbox>

Listbox.svelte

<script lang="ts">
  import { createListbox } from 'louisette'
  import { setContext } from 'svelte'

  export let label: string
  export let multiple: boolean = false
  export let value: string | string[] = ''

  const listboxContext = createListbox({ multiple })
  setContext('listbox', listboxContext)

  const { listboxAttrs, selected: selectedList } = listboxContext

  // 2-way binding to expose the selected value(s) to the parent component
  $: value = multiple ? $selectedList : $selectedList[0] ?? ''
</script>

<div
  class="flex max-w-sm flex-col gap-2 overflow-clip rounded-lg border border-neutral-200 bg-white p-4 text-neutral-900 dark:border-neutral-600 dark:bg-neutral-800 dark:text-neutral-100"
  {...$listboxAttrs}
  aria-label={label}
>
  <slot />
</div>

ListboxItem.svelte

<script lang="ts">
  import type { Listbox } from 'louisette'
  import { getContext } from 'svelte'

  export let value: string

  const {
    optionAttrs,
    selected: selectedList,
    activeDescendant,
  } = getContext<Listbox>('listbox')
</script>

<div
  class="flex cursor-pointer items-center justify-between gap-4 rounded-md p-2 text-sm transition-colors hover:bg-neutral-200 focus:outline-none focus-visible:ring focus-visible:ring-accent-500 focus-visible:ring-opacity-50 dark:hover:bg-neutral-700"
  class:is-active-descendant={$activeDescendant === value}
  {...$optionAttrs(value)}
>
  <span>
    <slot />
  </span>
  {#if $selectedList.includes(value)}
    <svg
      class="text-primary-500 dark:text-primary-400 ml-2 h-4 w-4"
      fill="currentColor"
      viewBox="0 0 20 20"
      aria-hidden="true"
    >
      <path
        clip-rule="evenodd"
        d="M10.707 14.707a1 1 0 01-1.414 0L5 10.414A1 1 0 016.414 9l3.293 3.293 6.293-6.293a1 1 0 111.414 1.414l-7 7z"
        fill-rule="evenodd"
      />
    </svg>
  {/if}
</div>

<style lang="postcss">
  .is-active-descendant {
    @apply bg-neutral-200 dark:bg-neutral-700;
  }
</style>