Settings Row
Settings sections, cards of label-plus-control rows on one control measure, and a settings × channels switch matrix.
- Status
- Since
0.1.0- Accessibility pattern
- headings + labelled rows
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Settings Row before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify settings-rowInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/settings-rowRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/card, @vegastack/switch.
Usage
import {
SettingsCard,
SettingsRow,
SettingsSection,
} from "@/components/ui/settings-row";
import { Switch } from "@/components/ui/switch";
<SettingsSection
title="Notifications"
description="Choose what you want to hear about."
>
<SettingsCard>
<SettingsRow label="Email" description="Product updates and tips.">
<Switch defaultChecked aria-label="Email notifications" />
</SettingsRow>
<SettingsRow label="SMS" description="Critical alerts only.">
<Switch aria-label="SMS alerts" />
</SettingsRow>
</SettingsCard>
</SettingsSection>;Anatomy
Settings Row is a compound component. Every exported part, with the
data-slot it renders (generated from the canonical source):
Examples
Anatomy
The settings layout is three flat parts that nest from the outside in. Each is a plain, server-safe
element with a forwarded ref and a data-slot for styling and targeting:
<SettingsSection title="…" description="…">
<SettingsCard>
<SettingsRow label="…" description="…">
{/* control: Switch, Input, Button, badge, or read-only value */}
</SettingsRow>
<SettingsRow label="…">{/* control */}</SettingsRow>
</SettingsCard>
</SettingsSection>SettingsSection— a titled group (data-slot="settings-section"). Renders an optionaltitleanddescriptionabove its children, with optionalactions("Sign out other devices", "New attribute") right-aligned on the title's row. Use one section per logical group (Account, Notifications, Danger zone).SettingsCard— upstream'sCard(data-slot="settings-card"), flattened for a divided list:py-0 gap-0, so rows sit edge to edge and each row's ownborder-bis the only divider. The radius, thebg-cardground and theborder border-borderhairline come fromCard, which is why a settings card and every other card on the page are the same object; the last row's border is collapsed so the list ends on the card edge rather than on a stray rule.SettingsRow— one setting (data-slot="settings-row"): alabeland optionaldescriptionon the left, the controlchildrenon the right, with aborder-bdivider. The row is its own named@container— it stacks or goes horizontal based on its own measured width, not the viewport, so a row placed in a narrow card (a settings panel in a split view, a dialog) stacks even on a wide screen, while the same row in a full-width page goes horizontal. Works standalone; noSettingsCardrequired.
All three are flat named exports — there is no dotted namespace.
Settings layouts
A bordered card grouping a mix of rows — a Switch, a Select, and another Switch:
A full section with a titled header above a card of switch rows:
Notifications
Choose what you want to hear about.
Native labels with controlId
Pass controlId when the row wraps a native form control with a matching id. The visual label
renders as a real <label htmlFor={controlId}>, so clicking the label focuses the input and screen
readers announce the pairing. Use labelProps to merge extra attributes (e.g. analytics hooks) onto
the generated label.
Control sizes
controlSize gives every row's control one measure once the row sits side by side — sm 160px,
md 256px, lg 320px — so stacked rows share one left edge and one right edge for their
controls. The first child of the control slot fills it (a trailing status icon or button beside it
keeps its own size). Left unset, a row holding a field control (input, input group, textarea,
select, combobox, date or people picker) takes md, found in CSS from the control's data-slot,
and anything else — a switch, a button, a read-only value — keeps its own width; auto opts a
field out. Below the row's side-by-side width the control spans the row. Never put a width class
on the control itself: pick a size.
Settings matrix
SettingsMatrix is settings × channels on one bordered surface: the column names ("Push",
"Email") once, in a header that sticks to the top of the scrolling page, a switch under each, and
the row labels on the left — not "Push" and "Email" repeated beside every switch. It sits where a
SettingsCard would. Each row passes its values (one per column) and an onChange(column, checked); disabled takes a whole row or one flag per column. Narrower than 28rem of its own
width, the header goes and each row stacks its switches under its label, each with its column name
beside it — part of the tap target.
What you hear about
In-app: always. Push goes to the mobile app.
Control variety
The control slot accepts any node — Input, Switch, a read-only value, a Badge, or a Button.
A row with no children renders label-only (no control slot), which suits headings such as a
danger-zone intro.
API Reference
SettingsRow
| Prop | Type | Default | Description |
|---|---|---|---|
label* | React.ReactNode | — | The row label rendered on the left (heading line of the row). |
children | React.ReactNode | — | The control rendered on the right — a Switch, Input, Button, badge,
or read-only value. |
controlId | string | — | ID of the form control rendered in children. When provided, the visual
row label renders as a real <label htmlFor={controlId}>. |
controlSize | "auto" | "lg" | "md" | "sm" | "md" for a field control (input, input group, textarea, select, combobox, searchable, country or region select, date or people picker, number field), otherwise "auto" | The control's width once the row sits side by side, so every row's control shares one
width and one right edge: sm 160px, md 256px, lg 320px — the first child of the control
slot fills it (a trailing status icon or button keeps its own size). auto keeps the
control's own width (a switch, a button, a read-only value). Below the row's side-by-side
width the control spans the row whatever the size. Never set a width class on the control
itself — pick a size. |
description | React.ReactNode | — | Optional supporting description rendered under the label (muted). |
labelProps | React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement> | — | Props merged onto the generated label element when controlId is provided. |
Data attributes and CSS variables on SettingsRow
| Attribute | Values |
|---|---|
data-control-size | mirrors a prop or state value |
data-slot | "settings-row" | "settings-row-control" | "settings-row-description" | "settings-row-label" |
SettingsMatrix
| Prop | Type | Default | Description |
|---|---|---|---|
columns* | readonly React.ReactNode[] | — | The column names ("Push", "Email"), shown once above the switches. |
rows* | readonly SettingsMatrixRow[] | — | The settings, one row each. |
label | React.ReactNode | — | The header above the row labels ("What you hear about"). Omit for an empty corner. |
Data attributes and CSS variables on SettingsMatrix
| Attribute | Values |
|---|---|
data-slot | "settings-matrix" | "settings-matrix-cell" | "settings-matrix-cell-label" | "settings-matrix-cells" | "settings-matrix-header" | "settings-matrix-label" | "settings-matrix-row" |
--settings-matrix-columns | CSS custom property |
Each row of rows:
| Prop | Type | Default | Description |
|---|---|---|---|
label* | React.ReactNode | — | The setting's name, on the left; with a column's name it names each switch ("Mentions Email"). |
values* | readonly boolean[] | — | On or off, one per column, in the columns' order. |
description | React.ReactNode | — | A muted line under the label. |
disabled | boolean | readonly boolean[] | false | Switches that cannot change — true for the whole row, or one flag per column. |
id | string | the row's index | A stable key for the row. |
onChange | ((column: number, checked: boolean) => void) | — | Called when a switch in this row changes, with its column's index. |
SettingsCard
| Prop | Type | Default | Description |
|---|---|---|---|
size | "default" | "sm" | — | |
variant | "default" | "outline" | — |
Data attributes and CSS variables on SettingsCard
| Attribute | Values |
|---|---|
data-slot | "settings-card" |
SettingsSection
| Prop | Type | Default | Description |
|---|---|---|---|
actions | React.ReactNode | — | Section-level actions ("Sign out other devices", "New attribute") on the SAME row as the title, right-aligned. They wrap under the title only when the row runs out of room. |
description | React.ReactNode | — | Supporting description rendered under the title (muted). |
title | React.ReactNode | — | Section heading rendered above the grouped content. |
titleAs | SettingsSectionTitleTag | 'h3' | Heading element the title renders as. Pick the level that continues the page's
outline — h2 directly under the page h1, h3 inside an h2 group, and so on.
The visual size never changes (it is the text-base font-medium role either way); only the
document structure does.
as rather than Base UI render on purpose: useRender calls React.useRef
internally, which would force 'use client' onto this file and cost the whole
settings family its server-safe status for a prop that only picks a tag name. |
Data attributes and CSS variables on SettingsSection
| Attribute | Values |
|---|---|
data-slot | "settings-section" | "settings-section-actions" | "settings-section-description" | "settings-section-header" | "settings-section-title" |
Accessibility
SettingsSectionrenders itstitleas a real heading —<h3>by default, or whatevertitleAsnames (h2…h6). A settings page nests sections at different depths, and the level is a document-structure fact the page owns: pick the one that continues the outline, so heading navigation stays meaningful. The visual size never changes. Keep one section per heading and order them logically.- Pass
controlIdwhen the row contains a form control with a matchingid; the row label becomes a real<label htmlFor={controlId}>. For composite controls that do not expose an input id, keep an explicitaria-labeloraria-labelledbyon the control itself. - Every
SettingsMatrixswitch is named by its row and its column ("Mentions Email") througharia-labelledby; the column header is visual only, and on a narrow matrix the column name is visible beside the switch. - Rows are separated by
border-borderrather than by elevation, so the divided list stays legible in high-contrast and forced-colors modes; the card's own hairline isCard'sborder border-border(BRD-1).
| Key | Action |
|---|---|
| Tab | Move focus through the controls in each row, top to bottom. |
| Enter / Space | Activate or toggle the focused control. |
| Contract | States tested |
|---|---|
| Behaviour | default, read-only |
| Accessibility | labeled, semantic-html |
| Visual | default |
Do / Don't
Settings Layout
The two-pane Settings frame — a sticky section nav and one 960px column where forms cap at 720px and lists fill; on a phone the nav comes first.
Timeline
Rail geometry for chronological records — a continuous connector with a node per entry; rows are Item parts, timestamps are RelativeTime.