Listbox
The listbox
component provides a way to select one or more options from a list of options.
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>