Menus and pickers
Pop-Up Button
Choose one option, or several, from a menu of choices.
import { Select, SelectContent, SelectItem, SelectLabel, SelectTrigger } from "macbetter/select"
const options = [
{ value: "automatic", label: "Automatically based on input" },
{ value: "when-scrolling", label: "When scrolling" },
{ value: "always", label: "Always" },
]
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 10 }}>
<Select items={options} defaultValue="when-scrolling">
<SelectLabel>Show scroll bars</SelectLabel>
<SelectTrigger style={{ width: 260 }} />
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}When to use it
A pop-up button shows the current choice and opens a menu of the others. Use it for a set of mutually exclusive options that would take too much room as radio buttons, like a setting with four values.
- Use it for choices, not commands. A list of actions belongs in a pull-down menu.
- For two or three options that people switch often, a segmented control is faster.
- Label it, and keep option names short enough to fit the button.
Usage
import { Select, SelectContent, SelectItem, SelectLabel, SelectTrigger } from "macbetter/select"SelectTrigger renders the value and its chevron on its own. SelectItem renders the checkmark beside the selected item. On the Mac the menu opens over the button with the selected item on top of it, its text lined up with the button's. Pass alignItemWithTrigger={false} to SelectContent to open it below instead; iOS and iPadOS, and any touch, always do.
Pass the options to Select as items too, as the examples do. Then the button can show the selected label before the menu has ever opened, including in the server-rendered page.
Groups
SelectGroup, SelectGroupLabel and SelectSeparator organize a long list.
import {
Select,
SelectContent,
SelectGroup,
SelectGroupLabel,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
type ListData,
} from "macbetter/select"
import { Fragment } from "react"
const families = [
{
label: "Sans Serif",
fonts: [
{ value: "sf-pro", label: "SF Pro" },
{ value: "helvetica", label: "Helvetica Neue" },
{ value: "avenir", label: "Avenir Next" },
],
},
{
label: "Serif",
fonts: [
{ value: "new-york", label: "New York" },
{ value: "georgia", label: "Georgia" },
],
},
{
label: "Monospaced",
fonts: [
{ value: "sf-mono", label: "SF Mono" },
{ value: "menlo", label: "Menlo" },
],
},
]
// Labels on the root let the button show "New York" before the menu has ever opened.
const items: ListData = families.map((family) => ({ type: "group", label: family.label, items: family.fonts }))
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 10 }}>
<Select items={items} defaultValue="new-york">
<SelectLabel>Font</SelectLabel>
<SelectTrigger style={{ width: 180 }} />
<SelectContent alignItemWithTrigger={false}>
{families.map((family, index) => (
<Fragment key={family.label}>
{index > 0 ? <SelectSeparator /> : null}
<SelectGroup>
<SelectGroupLabel>{family.label}</SelectGroupLabel>
{family.fonts.map((font) => (
<SelectItem key={font.value} value={font.value}>
{font.label}
</SelectItem>
))}
</SelectGroup>
</Fragment>
))}
</SelectContent>
</Select>
</div>
)
}Multiple
Set multiple to let people pick several. The menu stays open while they toggle items, and SelectValue's maxLabels summarizes a long selection.
import { Select, SelectContent, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "macbetter/select"
const events = [
{ value: "mentions", label: "Mentions" },
{ value: "replies", label: "Replies" },
{ value: "reactions", label: "Reactions" },
{ value: "new-members", label: "New members" },
]
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 10 }}>
<Select items={events} multiple defaultValue={["mentions", "replies"]} placeholder="Nothing">
<SelectLabel>Notify me about</SelectLabel>
<SelectTrigger style={{ width: 220 }}>
<SelectValue maxLabels={2} />
</SelectTrigger>
<SelectContent>
{events.map((event) => (
<SelectItem key={event.value} value={event.value}>
{event.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}Plain
variant="plain" drops the bezel: text and a small chevron, for toolbars, sort controls and table headers. It's what auto uses on iOS and iPadOS.
import { Select, SelectContent, SelectItem, SelectLabel, SelectTrigger } from "macbetter/select"
const sorts = [
{ value: "date", label: "Date" },
{ value: "name", label: "Name" },
{ value: "size", label: "Size" },
{ value: "kind", label: "Kind" },
]
export default function Example() {
return (
<div style={{ display: "flex", alignItems: "center", gap: 8, whiteSpace: "nowrap" }}>
<Select items={sorts} defaultValue="date">
<SelectLabel>Sort by</SelectLabel>
<SelectTrigger variant="plain" />
<SelectContent>
{sorts.map((sort) => (
<SelectItem key={sort.value} value={sort.value}>
{sort.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}On a phone
Set presentation="auto" on Select to show the list as a bottom sheet on small touch screens, with a popover everywhere else. presentation="sheet" always uses a sheet. Swipe the sheet down to dismiss it without changing the value.
import { Select, SelectContent, SelectItem, SelectLabel, SelectTrigger } from "macbetter/select"
const repeats = [
{ value: "never", label: "Never" },
{ value: "daily", label: "Every Day" },
{ value: "weekdays", label: "Every Weekday" },
{ value: "weekly", label: "Every Week" },
{ value: "monthly", label: "Every Month" },
{ value: "yearly", label: "Every Year" },
]
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 10 }}>
<Select items={repeats} defaultValue="weekly" presentation="auto">
<SelectLabel>Repeat</SelectLabel>
<SelectTrigger />
<SelectContent>
{repeats.map((repeat) => (
<SelectItem key={repeat.value} value={repeat.value}>
{repeat.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}Listbox
When the choices should stay on screen, use Listbox. It's the same choice in a list that's always visible: a bordered table with an accent selection on the Mac, and an inset list with a checkmark on iOS. The selection turns gray when the list doesn't have focus, as on the Mac.
import { Listbox, ListboxItem } from "macbetter/listbox"
export default function Example() {
return (
<div style={{ display: "grid", gap: 6, width: 240 }}>
<span id="browser-label">Default web browser</span>
<Listbox aria-labelledby="browser-label" defaultValue="safari">
<ListboxItem value="safari">Safari</ListboxItem>
<ListboxItem value="firefox">Firefox</ListboxItem>
<ListboxItem value="chrome">Chrome</ListboxItem>
<ListboxItem value="arc">Arc</ListboxItem>
</Listbox>
</div>
)
}import { Listbox, ListboxItem } from "macbetter/listbox"Platform behavior
- macOS: a bezeled button with the value and an accent square at its trailing edge holding a double chevron. Its menu is a Mac menu that always keeps a check column, so items don't shift as the value changes.
- iOS and iPadOS: the menu picker. The value is plain accent-colored text with an up-and-down chevron, and the menu is the roomy iOS menu with a checkmark beside the choice.
variant="bezeled"gives it a gray capsule instead. presentation="auto"onSelectshows the list as a bottom sheet on small touch screens.- Right to left, the check column and the Mac's text alignment mirror.
Accessibility
The trigger is a combobox named by SelectLabel. Enter, Space and the arrow keys open the list, typing selects by name even while it's closed, Enter chooses, and Escape closes without changing the value. With name, the value submits with a form.
API
SelectTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "auto" | "bezeled" | "plain" | "auto" | Bezeled on the Mac and plain on touch, or one of them everywhere. |
size | "sm" | "md" | "lg" | "md" | The control size. |
children | ReactNode | <SelectValue /> | What the button shows. The cap is always added. |
Other parts
| Prop | Type | Default | Description |
|---|---|---|---|
Select | items, value, defaultValue, multiple, presentation, name | — | The root. items lets the button show labels before the list opens. presentation is popover, sheet or auto. |
SelectContent | alignItemWithTrigger: boolean | "auto" | "auto" | Opens the menu over the button with the selected item on it. auto does that on the Mac only. |
SelectItem | value, label, disabled | — | An option. Renders its own checkmark. |
SelectItemIcon | mirror | — | An icon before the label. |
SelectGroup, SelectGroupLabel, SelectSeparator | — | — | Organize the list. |
SelectInput, SelectEmpty, SelectStatus | — | — | A search field inside the list, and its messages. |
Listbox, ListboxItem | value, multiple | — | The always-visible list. ListboxItem renders its own checkmark. |
Styling hooks
| Prop | Type | Default | Description |
|---|---|---|---|
data-selected | item | — | The chosen item, and its checkmark. |
data-placeholder | trigger and value | — | Nothing is chosen yet. |
data-open | trigger | — | The list is open. |
--mb-select-cap-inset | length | 4px | Space around the accent cap. |
--mb-select-content-shift | length | — | How far the Mac menu moves to line its text up with the button's. |