Design system

List dropdown

A borderless text trigger that opens a short list and shows the chosen value.

import { ListDropdown } from "@/ui";

ListDropdown is a small wrapper around the dropdown menu. The trigger is plain 12px text with a 12px caret and no border, so it can sit inside a header or a line of text without looking like a form field. The list is the product dropdown, with a blue check on the chosen option.

Use it for a quiet choice inside a panel header, such as which version of a plan you are looking at. For a choice in a form, use Dropdown. For a menu of actions, use the dropdown menu directly.

Usage

Pass the options, the current value and onChange. The trigger shows the label of the option whose value matches.

const [version, setVersion] = useState(3);

<ListDropdown
  options={[
    { value: 1, label: "v1" },
    { value: 2, label: "v2" },
    { value: 3, label: "v3 · Latest" },
  ]}
  value={version}
  onChange={setVersion}
/>

Custom trigger text

renderLabel receives the selected option and returns what the trigger shows. Use it to name the setting next to its value. The option is undefined when nothing matches value, so handle that case.

<ListDropdown
  options={sorts}
  value={sort}
  onChange={setSort}
  renderLabel={(o) => `Sort by: ${o?.label ?? "None"}`}
/>

Long lists

The list is capped at 280px and scrolls after that. If people need to search a list this long, use a field with search instead.

<ListDropdown options={campaigns} value={campaign} onChange={setCampaign} />

States

<ListDropdown options={[{ value: 1, label: "v1 · Latest" }]} value={1} disabled />

<ListDropdown options={sorts} value={undefined} onChange={setSort} renderLabel={(o) => o?.label ?? "Choose a sort"} />
StateLook
Default12px --color-text-primary, 6px by 12px padding, 8px corners, no background.
Hover--color-grey-50 background.
OpenThe panel opens below, lined up with the trigger's left edge, 4px away. The chosen option has a bold 16px check in --color-primary-500.
DisabledText and caret in --color-grey-300. The trigger cannot be clicked or focused into opening. The workbook disables it while a plan has only one version.
No matchWith no renderLabel, the trigger shows only the caret. Always give a renderLabel fallback when value can be empty.
No optionsThe trigger still renders, but nothing opens.

Props

PropTypeDefaultNotes
optionsArray<{ value: string | number, label: string }>[]value is compared with ===.
valuestring | numberThe selected option's value.
onChange(value) => voidCalled with the chosen option's value, also when it is already selected.
renderLabel(option | undefined) => ReactNodeReplaces the trigger text.
disabledbooleanfalse
classNamestringGoes on the wrapper <div>, not the trigger. For layout only.

No other props are passed on. In particular there is no aria-label, align or placeholder prop.

Accessibility

  • The trigger is a native <button> and the list is the product dropdown, so Enter, Space and the arrow keys open it, the arrow keys move through options, Escape closes it and focus returns to the trigger. See the dropdown menu for the full list.
  • The trigger's accessible name is only the visible text, such as "v3 · Latest". Because there is no aria-label prop, use renderLabel to put the setting's name in the text ("Sort by: Pipeline") when the value alone does not explain itself.
  • The chosen option is shown with a drawn check, not aria-checked, so a screen reader does not hear which option is selected. The trigger text is the only place it is announced.

The component's CSS still has rules for its own older menu (.list-dropdown__menu, .list-dropdown__item). They are no longer used since the list moved to the product dropdown, and can be deleted.

Do and don't

  • Do keep it for short lists of plain text options, up to about ten.
  • Do make the trigger text say what is being chosen, with renderLabel if needed.
  • Don't use it in a form. Form fields need a visible label and a border; use Dropdown.
  • Don't use it for actions. It shows a chosen value, so every option should be a value, not a verb.

On this page