Menu

Site and documentation links

Search documentation

Type to filter pages and sections. Use the arrow keys to move and Enter to open.

    Listbox

    A list of options where one can be selected, with typeahead.

    Analytics
    Billing
    Checkout
    Docs
    Onboarding
    View code
    import { Listbox, ListboxOption } from '@syncclouds/ui'
    
    const PROJECTS = ['Analytics', 'Billing', 'Checkout', 'Docs', 'Onboarding']
    
    export function ListboxDemo() {
      return (
        <Listbox aria-label="Project" defaultValue="billing" className="w-56">
          {PROJECTS.map(name => (
            <ListboxOption key={name} value={name.toLowerCase()}>
              {name}
            </ListboxOption>
          ))}
        </Listbox>
      )
    }

    Installation

    npm install @syncclouds/ui

    Usage

    import { Listbox, ListboxOption } from '@syncclouds/ui'
    <Listbox aria-label="Project" defaultValue="billing">
      <ListboxOption value="analytics">Analytics</ListboxOption>
      <ListboxOption value="billing">Billing</ListboxOption>
    </Listbox>

    Anatomy

    Listbox
    └── ListboxOption

    Controlled

    Keep the value in your own state. Disabled options are skipped.

    Berlin
    Karachi
    New York
    Singapore
    Sydney

    Time zone: Karachi

    View code
    'use client'
    
    import { useState } from 'react'
    import { Listbox, ListboxOption } from '@syncclouds/ui'
    
    const TIMEZONES = [
      { value: 'europe-berlin', label: 'Berlin' },
      { value: 'asia-karachi', label: 'Karachi' },
      { value: 'america-new-york', label: 'New York' },
      { value: 'asia-singapore', label: 'Singapore' },
      { value: 'australia-sydney', label: 'Sydney', disabled: true }
    ]
    
    export function ListboxControlled() {
      const [value, setValue] = useState('asia-karachi')
      const selected = TIMEZONES.find(zone => zone.value === value)
    
      return (
        <div className="flex flex-col items-center gap-4">
          <Listbox aria-label="Time zone" value={value} onValueChange={setValue} className="w-56">
            {TIMEZONES.map(zone => (
              <ListboxOption key={zone.value} value={zone.value} disabled={zone.disabled}>
                {zone.label}
              </ListboxOption>
            ))}
          </Listbox>
          <p className="text-sm">Time zone: {selected?.label}</p>
        </div>
      )
    }

    Key log

    onKeyAction reports every key that moved the selection, with the new option.

    Analytics
    Billing
    Checkout
    Docs
    Onboarding
    1. Focus the list and press a key.
    View code
    'use client'
    
    import { useState } from 'react'
    import { Listbox, ListboxOption } from '@syncclouds/ui'
    
    const PROJECTS = ['Analytics', 'Billing', 'Checkout', 'Docs', 'Onboarding']
    
    /* onKeyAction reports every key that moved the selection. */
    export function ListboxKeyLog() {
      const [log, setLog] = useState<string[]>([])
    
      return (
        <div className="flex w-full max-w-md gap-6">
          <Listbox
            aria-label="Project"
            defaultValue="analytics"
            className="w-44 shrink-0"
            onKeyAction={({ key, label }) =>
              setLog(entries => [`${key} moved to ${label}`, ...entries].slice(0, 5))
            }
          >
            {PROJECTS.map(name => (
              <ListboxOption key={name} value={name.toLowerCase()}>
                {name}
              </ListboxOption>
            ))}
          </Listbox>
          <ol aria-label="Key log" aria-live="polite" className="font-mono text-sm">
            {log.length === 0 && <li>Focus the list and press a key.</li>}
            {log.map((entry, i) => (
              <li key={`${entry}-${i}`}>{entry}</li>
            ))}
          </ol>
        </div>
      )
    }

    Without the theme

    Small
    Medium
    Large
    Extra large
    View code
    import { Listbox, ListboxOption } from '@syncclouds/ui'
    
    const SIZES = ['Small', 'Medium', 'Large', 'Extra large']
    
    export function ListboxUnstyled() {
      return (
        <Listbox
          aria-label="Size"
          defaultValue="medium"
          className="w-48 rounded-lg border border-zinc-300 p-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:border-zinc-600"
        >
          {SIZES.map(size => (
            <ListboxOption
              key={size}
              value={size.toLowerCase()}
              className="cursor-pointer rounded-md px-3 py-1.5 text-sm text-zinc-900 data-[state=selected]:bg-indigo-600 data-[state=selected]:text-white dark:text-zinc-100 dark:data-[state=selected]:bg-indigo-400 dark:data-[state=selected]:text-zinc-950"
            >
              {size}
            </ListboxOption>
          ))}
        </Listbox>
      )
    }

    Accessibility

    Follows the WAI-ARIA Listbox pattern.

    KeysWhat they do
    TabMoves focus into and out of the list. It is one tab stop.
    Arrow Down, Arrow UpSelects the next or previous option, wrapping at the ends.
    Home, EndSelects the first or last option.
    LettersSelects the next option whose name starts with what you type.
    • Selection follows focus, as the APG recommends for single-select lists.
    • Name the list with aria-label or aria-labelledby.

    API reference

    Every part also accepts the props of the element it renders, including className and ref.

    Listboxrenders <div>

    The list. It takes focus itself and points aria-activedescendant at the selected option.

    PropTypeDefaultDescription
    valuestringnoneThe selected option, when you control it.
    defaultValuestring""The option selected at first.
    onValueChange(value: string) => voidnoneCalled when the selection changes, by keyboard or pointer.
    onKeyAction(action: { key: string; value: string; label: string }) => voidnoneCalled after a key moves the selection, with the key and the new option.
    Data attributeValues
    data-scope"listbox"
    data-part"root"

    ListboxOptionrenders <div>

    One option. Its text is used for typeahead.

    PropTypeDefaultDescription
    valuestringnoneRequired. Identifies the option.
    disabledbooleanfalseSkipped by the keyboard and ignores clicks.
    Data attributeValues
    data-state"selected" | "unselected"
    data-disabledpresent when disabled
    Built by Syncclouds. Syncclouds UI is in development.