Content
List
Rows of settings, navigation and choices, grouped the way iOS Settings and macOS System Settings group them.
import {
List,
ListRow,
ListRowAccessory,
ListRowIcon,
ListRowTitle,
ListRowValue,
ListSection,
} from "macbetter/list"
import { Switch } from "macbetter/switch"
import {
AntennaIcon,
BellIcon,
BluetoothIcon,
HourglassIcon,
LinkIcon,
MoonIcon,
PlaneIcon,
SettingsIcon,
Volume2Icon,
WifiIcon,
} from "lucide-react"
export default function Example() {
return (
<List style={{ width: "100%", maxWidth: 560, marginInline: "auto" }}>
<ListSection>
<ListRow>
<ListRowIcon color="orange">
<PlaneIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>Airplane Mode</ListRowTitle>
<ListRowAccessory>
<Switch aria-label="Airplane Mode" />
</ListRowAccessory>
</ListRow>
<ListRow href="#wi-fi">
<ListRowIcon color="blue">
<WifiIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>Wi-Fi</ListRowTitle>
<ListRowValue>macbetter HQ</ListRowValue>
<ListRowAccessory type="disclosure" />
</ListRow>
<ListRow href="#bluetooth">
<ListRowIcon color="blue">
<BluetoothIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>Bluetooth</ListRowTitle>
<ListRowValue>On</ListRowValue>
<ListRowAccessory type="disclosure" />
</ListRow>
<ListRow href="#cellular">
<ListRowIcon color="green">
<AntennaIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>Cellular</ListRowTitle>
<ListRowAccessory type="disclosure" />
</ListRow>
<ListRow href="#hotspot">
<ListRowIcon color="green">
<LinkIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>Personal Hotspot</ListRowTitle>
<ListRowValue>Off</ListRowValue>
<ListRowAccessory type="disclosure" />
</ListRow>
</ListSection>
<ListSection>
<ListRow href="#notifications">
<ListRowIcon color="red">
<BellIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>Notifications</ListRowTitle>
<ListRowAccessory type="disclosure" />
</ListRow>
<ListRow href="#sounds">
<ListRowIcon color="pink">
<Volume2Icon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>Sounds & Haptics</ListRowTitle>
<ListRowAccessory type="disclosure" />
</ListRow>
<ListRow href="#focus">
<ListRowIcon color="indigo">
<MoonIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>Focus</ListRowTitle>
<ListRowAccessory type="disclosure" />
</ListRow>
<ListRow href="#screen-time">
<ListRowIcon color="indigo">
<HourglassIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>Screen Time</ListRowTitle>
<ListRowAccessory type="disclosure" />
</ListRow>
</ListSection>
<ListSection footer="Updates download overnight while your device is charging and connected to Wi-Fi.">
<ListRow href="#general">
<ListRowIcon color="gray">
<SettingsIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>General</ListRowTitle>
<ListRowAccessory type="disclosure" />
</ListRow>
</ListSection>
</List>
)
}When to use it
Use a list for settings and for navigating into more detail: each row names one thing, shows its current value at the trailing edge, and either holds its own control (a switch) or takes you somewhere (a chevron). Group related rows into sections, and use a section footer to explain what a setting does rather than a long row title.
Use a Table when people compare several attributes of many items, and an Outline when the items nest.
- Keep row titles short, in title case, and start with the noun people scan for.
- Put the current value in
ListRowValue, not in the title. - A row that navigates gets a disclosure chevron. A row that acts right away, like Sign Out, doesn't.
Usage
import {
List,
ListRow,
ListRowAccessory,
ListRowIcon,
ListRowSubtitle,
ListRowTitle,
ListRowValue,
ListSection,
} from "macbetter/list"Rows that navigate, act or hold a control
A row with href is a link, and with render it's your router's link component: render={<Link href="/wi-fi" />}. A row with onClick is a button. A row with neither is static, which is what a row holding its own control needs: a Switch inside a button would be a button inside a button. Name the control after the row's title.
Icon tiles
ListRowIcon draws the rounded, colored tile from Settings. Its color is a system color (red, orange, yellow, green, mint, teal, cyan, blue, indigo, purple, pink, brown, gray) or accent. Put a white glyph inside; the tile is hidden from screen readers, so the title names the row.
Subtitles
import {
List,
ListRow,
ListRowAccessory,
ListRowIcon,
ListRowSubtitle,
ListRowTitle,
ListSection,
} from "macbetter/list"
import { AtSignIcon, CloudIcon, MailIcon } from "lucide-react"
export default function Example() {
return (
<List style={{ width: "100%", maxWidth: 560, marginInline: "auto" }}>
<ListSection header="Accounts">
<ListRow href="#icloud">
<ListRowIcon color="cyan">
<CloudIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>iCloud</ListRowTitle>
<ListRowSubtitle>Mail, Contacts, Calendars, Notes</ListRowSubtitle>
<ListRowAccessory type="disclosure" />
</ListRow>
<ListRow href="#work">
<ListRowIcon color="indigo">
<MailIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>macbetter</ListRowTitle>
<ListRowSubtitle>Mail, Calendars</ListRowSubtitle>
<ListRowAccessory type="disclosure" />
</ListRow>
<ListRow href="#personal">
<ListRowIcon color="accent">
<AtSignIcon aria-hidden="true" />
</ListRowIcon>
<ListRowTitle>Personal</ListRowTitle>
<ListRowSubtitle>Mail</ListRowSubtitle>
<ListRowAccessory type="disclosure" />
</ListRow>
</ListSection>
</List>
)
}Choices
A checkmark marks the chosen row in a short list of options. Mark the chosen row with aria-pressed so it's announced, or use a Radio group when the options belong to a form.
"use client"
import { List, ListRow, ListRowAccessory, ListRowTitle, ListSection } from "macbetter/list"
import { useState } from "react"
const appearances = ["Light", "Dark", "Automatic"]
export default function Example() {
const [appearance, setAppearance] = useState("Automatic")
return (
<List style={{ width: "100%", maxWidth: 560, marginInline: "auto" }}>
<ListSection header="Appearance" footer="Automatic switches to Dark at sunset.">
{appearances.map((option) => (
<ListRow key={option} aria-pressed={appearance === option} onClick={() => setAppearance(option)}>
<ListRowTitle>{option}</ListRowTitle>
{appearance === option ? <ListRowAccessory type="checkmark" /> : null}
</ListRow>
))}
</ListSection>
</List>
)
}Styles
inset-grouped, the default, floats rounded groups inside the margins. grouped runs the groups edge to edge between hairlines. plain drops the groups and pins each section header to the top while its rows scroll, like Contacts. Inside a navigation stack the headers pin just below the bar, using the --mb-content-inset-top the stack sets.
import { List, ListRow, ListRowAccessory, ListRowTitle, ListRowValue, ListSection } from "macbetter/list"
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", width: "100%" }}>
<List variant="grouped">
<ListSection header="Grouped" footer="Rows run edge to edge between hairlines.">
<ListRow href="#name">
<ListRowTitle>Name</ListRowTitle>
<ListRowValue>Kai’s MacBook Pro</ListRowValue>
<ListRowAccessory type="disclosure" />
</ListRow>
<ListRow>
<ListRowTitle>Model</ListRowTitle>
<ListRowValue>MacBook Pro 14-inch</ListRowValue>
</ListRow>
</ListSection>
</List>
<List variant="plain">
<ListSection header="A">
<ListRow href="#ada">
<ListRowTitle>Ada Okafor</ListRowTitle>
</ListRow>
<ListRow href="#alex">
<ListRowTitle>Alex Moreau</ListRowTitle>
</ListRow>
</ListSection>
<ListSection header="B">
<ListRow href="#bea">
<ListRowTitle>Bea Lindqvist</ListRowTitle>
</ListRow>
</ListSection>
</List>
</div>
)
}Platform behavior
- iOS and iPadOS. 17-point text, rows at least 48 points tall, and a 30-point icon tile. The list paints the grouped background and the groups sit on it in the secondary grouped color. Separators start under the title, past an icon tile, artwork or avatar, and run to the trailing edge, as UIKit draws them. Section headers are secondary footnote text, not capitals. A pressed row fills edge to edge and the hairlines around it step aside.
- macOS. The System Settings look: 13-point text, rows 36 points tall (40 with an icon), a 20-point icon tile, and groups drawn as a faint box with a hairline edge on the window background. Separators are inset from both edges and don't skip the icon. Section headers are 13-point semibold in the primary label color, as System Settings sets them.
- Disclosure chevrons, checkmarks and the info glyph are drawn at each platform's size and weight; checkmarks and the info glyph take the accent color.
Accessibility
- Rows are real links and buttons, so Tab reaches them and Enter (and Space, for buttons) activates them. Static rows aren't focusable; the control inside them is.
- A row's accessible name is its text: title, subtitle and value read together, like "Wi-Fi, macbetter HQ".
- Icon tiles and accessory glyphs are
aria-hidden. - A disabled row's button can't be pressed and its contents dim.
- Keyboard focus draws the accent ring inside the row, so the group's rounded corners don't clip it.
API
List
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "inset-grouped" | "grouped" | "plain" | "inset-grouped" | How the sections are drawn. |
...props | ComponentProps<"div"> | — | Passed to the element. |
ListSection
| Prop | Type | Default | Description |
|---|---|---|---|
header | ReactNode | — | Text above the group. |
footer | ReactNode | — | Text below the group that explains its settings. |
ListRow
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | Makes the row a link. |
render | ReactElement | function | — | Renders the row as another element, like a router link. Makes the row interactive. |
onClick | (event) => void | — | Makes the row a button. |
disabled | boolean | false | Dims the row and stops presses. |
ListRowIcon, ListRowAccessory
| Prop | Type | Default | Description |
|---|---|---|---|
color | SystemColor | "gray" | "accent" | "gray" | ListRowIcon: the tile's color. |
type | "disclosure" | "checkmark" | "info" | — | ListRowAccessory: a standard glyph. Leave it out and pass a control as children. |
ListRowTitle, ListRowSubtitle and ListRowValue take the props of a span.
Data attributes and CSS variables
| Attribute or variable | On | Meaning |
|---|---|---|
data-variant | .mb-list | inset-grouped, grouped or plain. |
data-interactive | .mb-list-row | The row is a link or a button. |
data-disabled | .mb-list-item | The row is disabled. |
data-color | .mb-list-row-icon | The tile's color. |
data-type | .mb-list-row-accessory | disclosure, checkmark or info. |
--mb-list-row-height, --mb-list-row-height-icon | platform | Minimum row heights. |
--mb-list-row-padding-inline | platform | Leading and trailing padding, which the headers and footers share. |
--mb-list-icon-size, --mb-list-icon-radius, --mb-list-icon-gap | platform | The icon tile. |
--mb-list-separator-start, --mb-list-separator-start-icon, --mb-list-separator-end | platform | Where hairlines start and end on macOS, and on iOS rows without a ListRowTitle. |
--mb-content-inset-top | inherited | Where a plain list's headers pin. Set by NavigationStack; 0 otherwise. |
--mb-list-bg, --mb-list-group-bg, --mb-list-pressed | platform | Surfaces and the pressed fill. |