Dropdown
Use Dropdown component to choose option(s) from a list.
Import
JSX
import Dropdown from '@box-kite/react/components/dropdown';Basic Dropdown
Choice
JSX
<Dropdown label="Choice" defaultValue={1}>
<Dropdown.Item value={1}>Option 1</Dropdown.Item>
<Dropdown.Item value={2}>Option 2</Dropdown.Item>
</Dropdown>Keyboard and roles
This is the APG combobox, whole — in both of its shapes. The trigger is a
role="combobox" that keeps DOM focus the entire time; the popup is a role="listbox" of role="option" rows, and the option the arrows are on is named by aria-activedescendant rather than focused. Add isSearchable and the combobox becomes the text field instead of a button. You write none of that — and if you were writing it by hand before, delete it.Give it a label
A combobox is not named by what it contains: the text in the trigger is its value. Pass
label and the component renders it and wires aria-labelledby — or pass your own aria-label in props. Without either, the control has no accessible name, exactly like an input with only a placeholder.Clear and Select all are options too
Dropdown.Unselect and Dropdown.SelectAll sit inside the listbox, so they carry role="option" and the arrow keys reach them like any other row.Searchable is the editable combobox
With
isSearchable the text field is the combobox — the role, the ARIA and anything you pass in props live on the input, so nothing focusable sits inside anything else focusable. It is a different keyboard map — the one under With isSearchable in the keyboard reference below: the printable keys type instead of navigating, and only Down and Up reach the listbox.Controlled Dropdown
JSX
const [selectedValue, setSelectedValue] = useState<number>(2);
<Dropdown value={selectedValue} onValueChange={(value) => setSelectedValue(value as number)}>
<Dropdown.Item value={1}>Option 1</Dropdown.Item>
<Dropdown.Item value={2}>Option 2</Dropdown.Item>
<Dropdown.Item value={3}>Option 3</Dropdown.Item>
</Dropdown>Unselect Item
JSX
<Dropdown>
<Dropdown.Unselect>Select</Dropdown.Unselect>
<Dropdown.Item value={1}>Option 1</Dropdown.Item>
<Dropdown.Item value={2}>Option 2</Dropdown.Item>
</Dropdown>Disabled
JSX
<Dropdown disabled width={50}>
<DropdownUnselect>Select</DropdownUnselect>
<DropdownItem value={1}>Option 1</DropdownItem>
<DropdownItem value={2}>Option 2</DropdownItem>
</Dropdown>Compact
JSX
<Dropdown variant="compact" width={40}>
<DropdownUnselect>Select</DropdownUnselect>
<DropdownItem value={1}>Option 1</DropdownItem>
<DropdownItem value={2}>Option 2</DropdownItem>
</Dropdown>Custom Variant (outlined)
JSX
// boxExtends.ts — register the variant, and export what you registered.
export const components = Box.components({
dropdown: {
variants: {
outlined: {
bgColor: 'transparent', b: 2, borderColor: 'indigo-500', color: 'indigo-600',
},
},
children: {
item: { variants: { outlined: { hover: { bgColor: 'indigo-50' }, selected: { bgColor: 'indigo-100' } } } },
items: { variants: { outlined: { b: 2, borderColor: 'indigo-500' } } },
},
},
});
// box.d.ts — teach TypeScript the name, or variant="outlined" stays a type error.
import { ExtractComponentsAndVariants } from '@box-kite/core/types';
import { components } from './boxExtends';
declare module '@box-kite/core/types' {
namespace Augmented {
interface ComponentsTypes extends ExtractComponentsAndVariants<typeof components> {}
}
}
// Use — the variant propagates to all children automatically.
<Dropdown variant="outlined">
<Dropdown.Item value={1}>Option 1</Dropdown.Item>
<Dropdown.Item value={2}>Option 2</Dropdown.Item>
</Dropdown>Searchable
User
Select
JSX
<Dropdown
label="User"
isSearchable
searchPlaceholder="Search users..."
width={50}
>
<DropdownUnselect>Select</DropdownUnselect>
<DropdownItem value={1}>John Doe</DropdownItem>
<DropdownItem value={2}>Joe Smith</DropdownItem>
<DropdownItem value={3}>Alice</DropdownItem>
<DropdownItem value={4}>Bob</DropdownItem>
</Dropdown>Searchable with Empty Item
User
Select
JSX
<Dropdown
label="User"
isSearchable
searchPlaceholder="Search users..."
width={50}
>
<DropdownEmptyItem>No options</DropdownEmptyItem>
<DropdownUnselect>Select</DropdownUnselect>
<DropdownItem value={1}>John Doe</DropdownItem>
<DropdownItem value={2}>Joe Smith</DropdownItem>
<DropdownItem value={3}>Alice</DropdownItem>
<DropdownItem value={4}>Bob</DropdownItem>
</Dropdown>Multiple Selection
JSX
<Dropdown multiple width={50}>
<DropdownEmptyItem>No options</DropdownEmptyItem>
<DropdownUnselect>Unselect All</DropdownUnselect>
<DropdownSelectAll>Select All</DropdownSelectAll>
<DropdownItem value={1}>John Doe</DropdownItem>
<DropdownItem value={2}>Joe Smith</DropdownItem>
<DropdownItem value={3}>Alice</DropdownItem>
<DropdownItem value={4}>Bob</DropdownItem>
</Dropdown>Multiple Selection with Checkboxes
JSX
<Dropdown multiple showCheckbox width={50}>
<DropdownEmptyItem>No options</DropdownEmptyItem>
<DropdownUnselect>Unselect All</DropdownUnselect>
<DropdownSelectAll>Select All</DropdownSelectAll>
<DropdownItem value={1}>John Doe</DropdownItem>
<DropdownItem value={2}>Joe Smith</DropdownItem>
<DropdownItem value={3}>Alice</DropdownItem>
<DropdownItem value={4}>Bob</DropdownItem>
</Dropdown>Multiple with Custom Display
JSX
<Dropdown multiple showCheckbox width={50}>
<DropdownDisplay />
<DropdownEmptyItem>No options</DropdownEmptyItem>
<DropdownUnselect>Unselect All</DropdownUnselect>
<DropdownSelectAll>Select All</DropdownSelectAll>
<DropdownItem value={1}>John Doe</DropdownItem>
<DropdownItem value={2}>Joe Smith</DropdownItem>
<DropdownItem value={3}>Alice</DropdownItem>
<DropdownItem value={4}>Bob</DropdownItem>
</Dropdown>Select
Data-driven dropdown — pass data + def instead of composing children. Wraps Dropdown internally.
Import
JSX
import Select from '@box-kite/react/components/select';Basic Select
User
JSX
const users = [
{ id: 1, name: 'John Doe', role: 'Admin' },
{ id: 2, name: 'Joe Smith', role: 'Editor' },
{ id: 3, name: 'Alice Brown', role: 'Viewer' },
];
<Select
label="User"
data={users}
def={{ valueKey: 'id', displayKey: 'name', placeholder: 'Pick a user...' }}
width={50}
/>Custom Item Display
JSX
<Select
data={users}
def={{
valueKey: 'id',
display: (user) => `${user.name} — ${user.role}`,
placeholder: 'Pick a user...',
}}
/>Multiple with Search
Users
Pick users...
JSX
<Select
data={users}
def={{
valueKey: 'id',
displayKey: 'name',
placeholder: 'Pick users...',
selectAllText: 'Select all',
emptyText: 'No users found',
selectedDisplay: (rows) =>
rows.length === 0 ? 'Pick users...' : `${rows.length} selected`,
}}
label="Users" multiple showCheckbox isSearchable searchPlaceholder="Search users..."
/>Dropdown props
Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on /box rather than repeated here.
| Prop | Type | Default | What it does |
|---|---|---|---|
name | string | — | What the selection submits under: the component renders a hidden input carrying it. |
defaultValue | TVal | TVal[] | — | What is selected to begin with, when the dropdown owns its own value. An array in multiple mode. |
value | TVal | TVal[] | — | Controlled selection. Pair it with onValueChange, or nothing the user picks sticks. |
defaultOpen | boolean | — | Open the listbox as soon as the dropdown mounts, highlighting the selected option the way a press on the trigger would. For a dropdown that only exists because the user asked to choose — a grid's cell editor — where a second press to open a list that is the whole control is a gesture nobody meant. |
multiple | boolean | false | Let more than one option be chosen. Enter and Space then toggle without closing the listbox. |
isSearchable | boolean | — | Turn the control into the APG *editable* combobox: a text field that filters the listbox, and a different pattern rather than a decoration — the field carries the role, the ARIA and your props. Printable keys type instead of jumping, Home/End and left/right move the caret, and Escape closes the listbox before it clears the field. |
searchPlaceholder | string | — | The placeholder for the isSearchable field. Not a name — see label. |
hideIcon | boolean | — | Drop the chevron. It is decoration either way: it is aria-hidden and never focusable. |
showCheckbox | boolean | false | Show checkbox for each item in multiple selection mode |
label | React.ReactNode | — | The dropdown's name, rendered above it and pointed at by aria-labelledby. A combobox is not named by what it contains — accname does not read a combobox at all, and the trigger text is its *value* — so without this (or your own aria-label) it has no accessible name. |
labelProps | BoxProps<'div'> | — | Styles for the wrapper the label and the trigger share. Only rendered when there is a label. |
itemsProps | BoxStyleProps | — | BoxProps applied to the opened items container (dropdown.items) |
iconProps | BoxStyleProps | — | BoxProps applied to the chevron icon container (dropdown.icon) |
onValueChange | ChangeHandler<TVal | TVal[] | undefined, DropdownValueReason> | — | Fires with the selection in whatever shape value takes — the whole array in multiple mode, the one chosen option otherwise, undefined for nothing — and why it changed. |
onChange | (value: TVal | undefined, values: TVal[]) => void | — | The older callback, whose first argument is the option *acted on* rather than the selection. Both fire, so either one can be passed. |
Dropdown.Display
| Prop | Type | Default | What it does |
|---|---|---|---|
childrenrequired | ((selectedValues: TVal[], isOpen: boolean) => React.ReactNode) | React.ReactNode | — | What the trigger shows: a node, or a function given the selected values and whether it is open. |
Dropdown keyboard
| Key | Result |
|---|---|
Down / Up (closed) | Opens, with the highlight on the selected option — or on the first or last when nothing is chosen. |
Alt + Down (closed) | Opens without moving the highlight. |
Enter / Space (closed) | Opens. The browser's own activation is suppressed, so it does not shut again. |
Home / End (closed) | Opens at the first or the last option. |
A printable character | Opens with the first option starting with it already highlighted. |
Down / Up (open) | Moves the highlight, wrapping at the ends and skipping disabled options. |
Home / End (open) | Jumps to the first or last option. |
Typing (open) | Typeahead. A longer buffer narrows; the same letter twice cycles through the options sharing it. |
Enter / Space (open) | Chooses the highlighted option and closes. In multiple mode it toggles and stays open. |
Alt + Up (open) | Chooses the highlighted option and closes. |
Escape | Closes, changing nothing. Focus never left the trigger, so nothing is restored. |
Tab (open) | Chooses the highlighted option, then moves on to the next control. |
With isSearchable
| Key | Result |
|---|---|
A printable character | Types into the field, which opens the listbox and filters it. No typeahead — the field owns the keys. |
Down / Up | Opens, or moves the highlight through what the filter left, wrapping and skipping disabled options. |
Home / End, Left / Right | Move the caret, and hand the highlight back to the field: no option is where you are any more. |
Space | Types a space. Only Enter chooses in this mode. |
Enter | Chooses the highlighted option and puts its text in the field. With nothing highlighted it does nothing. |
Escape | Closes the listbox, keeping what was typed. Pressed again on a closed one, it clears the field. |
Dropdown accessibility
Implements the WAI-ARIA APG pattern.
role="combobox"witharia-expanded,aria-controlsandaria-activedescendanton the trigger, androle="listbox"/role="option"witharia-selectedon the popup.- Nothing names a combobox for you — accname does not read its contents, and the trigger text is its *value* — so pass
label, or anaria-labelof your own inprops. - DOM focus never leaves the trigger (or, when searchable, the field), so no option is a tab stop and the keyboard is never stranded in the popup. The popup itself is a sibling of the trigger in the browser's top layer, so it needs no portal and keeps the theme and the direction around it.
Swept with axe on every release, in 3 states:
Dropdown (closed), Dropdown (open), Dropdown (searchable, open). No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.Dropdown style tree
Every part the component draws is a node with a name, so a default can be restyled with
Box.components() instead of a selector — and a variant is a name too.dropdownvariants: compactdropdown.itemsvariants: closed, closedUpdropdown.itemvariants: highlighted, compact, multipledropdown.unselectvariants: highlighted, compactdropdown.selectAllvariants: highlighted, compactdropdown.emptyItemvariants: compactdropdown.icon