Menus and pickers
Combo Box
Type a value or choose one from a list of suggestions.
import {
Combobox,
ComboboxContent,
ComboboxControl,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxTrigger,
} from "macbetter/combo-box"
const sizes = ["9", "10", "11", "12", "13", "14", "18", "24", "36", "48", "64"]
export default function Example() {
return (
<Combobox defaultValue="13">
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<ComboboxLabel>Font size</ComboboxLabel>
<ComboboxControl style={{ width: 120 }}>
<ComboboxInput placeholder="Size" />
<ComboboxTrigger aria-label="Show font sizes" />
</ComboboxControl>
</div>
<ComboboxContent>
<ComboboxEmpty>No matching sizes</ComboboxEmpty>
<ComboboxList>
{sizes.map((size) => (
<ComboboxItem key={size} value={size} label={size}>
{size}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}When to use it
A combo box is a text field with a list attached. Use it when the list is long enough that typing finds an item faster than scrolling, like a city or a font size.
- Use
Comboboxwhen the value must be one of the items; what people type only filters the list. - Use
Autocompletewhen anything typed is valid and the list only suggests, like a search or a destination. - For a short list that doesn't need typing, use a pop-up button.
Usage
import {
Combobox,
ComboboxContent,
ComboboxControl,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxTrigger,
} from "macbetter/combo-box"ComboboxControl holds the input and places the trigger and the clear button at its trailing edge. ComboboxTrigger and ComboboxClear draw their own glyphs; name both with aria-label. ComboboxItem renders a checkmark beside the selected item.
Groups and clearing
Groups that filtering leaves empty disappear with their separators. ComboboxClear appears while there's a value or text.
import {
Combobox,
ComboboxClear,
ComboboxContent,
ComboboxControl,
ComboboxEmpty,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxSeparator,
ComboboxTrigger,
} from "macbetter/combo-box"
import { Fragment } from "react"
const regions = [
{
label: "Americas",
cities: [
{ value: "America/New_York", label: "New York" },
{ value: "America/Chicago", label: "Chicago" },
{ value: "America/Los_Angeles", label: "Los Angeles" },
],
},
{
label: "Europe",
cities: [
{ value: "Europe/London", label: "London" },
{ value: "Europe/Paris", label: "Paris" },
{ value: "Europe/Berlin", label: "Berlin" },
],
},
{
label: "Asia and Pacific",
cities: [
{ value: "Asia/Tokyo", label: "Tokyo" },
{ value: "Asia/Singapore", label: "Singapore" },
{ value: "Australia/Sydney", label: "Sydney" },
],
},
]
export default function Example() {
return (
<Combobox defaultValue="Europe/Paris">
<div style={{ display: "grid", gap: 6 }}>
<ComboboxLabel>Closest city</ComboboxLabel>
<ComboboxControl>
<ComboboxInput placeholder="Search cities" />
<ComboboxClear aria-label="Clear city" />
<ComboboxTrigger aria-label="Show cities" />
</ComboboxControl>
</div>
<ComboboxContent>
<ComboboxEmpty>No matching cities</ComboboxEmpty>
<ComboboxList>
{regions.map((region, index) => (
<Fragment key={region.label}>
{index > 0 ? <ComboboxSeparator /> : null}
<ComboboxGroup>
<ComboboxGroupLabel>{region.label}</ComboboxGroupLabel>
{region.cities.map((city) => (
<ComboboxItem key={city.value} value={city.value} label={city.label}>
{city.label}
</ComboboxItem>
))}
</ComboboxGroup>
</Fragment>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Autocomplete
The Autocomplete parts share the look, without the check column.
import {
Autocomplete,
AutocompleteContent,
AutocompleteInput,
AutocompleteItem,
AutocompleteLabel,
AutocompleteList,
ComboboxControl,
} from "macbetter/combo-box"
const destinations = ["Lisbon", "Lima", "Lyon", "Ljubljana", "Los Angeles", "Luang Prabang"]
export default function Example() {
return (
<Autocomplete>
<div style={{ display: "grid", gap: 6 }}>
<AutocompleteLabel>Destination</AutocompleteLabel>
<ComboboxControl>
<AutocompleteInput placeholder="Where to?" />
</ComboboxControl>
</div>
<AutocompleteContent>
<AutocompleteList>
{destinations.map((city) => (
<AutocompleteItem key={city} value={city}>
{city}
</AutocompleteItem>
))}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
)
}On a phone
Set presentation="auto" on the root to show the list as a bottom sheet on small touch screens. The input moves into the sheet's header, so there's still one field and one query.
import {
Combobox,
ComboboxContent,
ComboboxControl,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxTrigger,
} from "macbetter/combo-box"
const languages = [
"Čeština",
"Dansk",
"Deutsch",
"English",
"Español",
"Français",
"Italiano",
"Nederlands",
"Norsk",
"Polski",
"Português",
"Suomi",
"Svenska",
"Türkçe",
]
export default function Example() {
return (
<Combobox defaultValue="English" presentation="auto">
<div style={{ display: "grid", gap: 6 }}>
<ComboboxLabel>Preferred language</ComboboxLabel>
<ComboboxControl>
<ComboboxInput placeholder="Search languages" />
<ComboboxTrigger aria-label="Show languages" />
</ComboboxControl>
</div>
<ComboboxContent>
<ComboboxEmpty>No matching languages</ComboboxEmpty>
<ComboboxList>
{languages.map((language) => (
<ComboboxItem key={language} value={language}>
{language}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}Platform behavior
- macOS: a text field with an accent cap holding a chevron, like a pop-up button's. The list is a Mac menu, at least as wide as the field.
- iOS and iPadOS: a filled, rounded field with a quiet chevron, and the roomy iOS menu.
presentation="auto"on the root shows the list as a sheet on small screens.
Accessibility
The input has the combobox role and keeps focus while the arrow keys move the highlight through the list. Enter chooses, Escape closes the list and restores the previous value, and results are announced when typing pauses. The field shows its focus ring whenever it's editing, as text fields do on the Mac.
API
| Prop | Type | Default | Description |
|---|---|---|---|
ComboboxControl | div props | — | The field. Positions the trigger and clear button. |
ComboboxTrigger | aria-label | — | Shows every item. Renders a chevron. |
ComboboxClear | aria-label | — | Clears the value and text. Renders a clear glyph. |
ComboboxContent | sideOffset | 4 | The list's popup. |
ComboboxItem | value, label, disabled | — | A choice. Renders its own checkmark. |
ComboboxItemText | — | — | Bolds the part of the label that matched. |
ComboboxEmpty, ComboboxStatus | — | — | What shows when nothing matches, and loading or error messages. |
Combobox | value, defaultValue, multiple, presentation | — | The root. presentation is popover, sheet or auto. |
Autocomplete… | — | — | The same parts for free text: Input, Trigger, Clear, Content, List, Item, Group, Empty. |
--mb-combo-box-width | length | 240px · 300px | The field's width on the Mac · on touch. |