Settings Row
A borders-only settings layout — titled sections, bordered cards, and label-plus-control rows for building account, workspace, and preference screens.
- Status
- Since
0.1.0- Accessibility pattern
- headings + labelled rows
Last updated
Install
Add Settings Row from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/settings-rowUsage
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. Use one section per logical group (Account, Notifications, Danger zone).SettingsCard— a borders-only container (data-slot="settings-card") that groups rows into a singlerounded-lgsurface. It collapses the last row's bottom border so the rows read as a divided list.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 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}>. |
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-slot | "settings-row" | "settings-row-control" | "settings-row-description" | "settings-row-label" |
SettingsCard
SettingsCard adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Data attributes and CSS variables on SettingsCard
| Attribute | Values |
|---|---|
data-slot | "settings-card" |
SettingsSection
| Prop | Type | Default | Description |
|---|---|---|---|
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-h4 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-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. - Borders-only by design: rows are separated by
border-border, not shadows, so the layout stays legible in high-contrast and forced-colors modes.
| 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 |