Skip to content
Component installs need the registry setup
VegaStack Design

Settings Row

Settings sections, cards of label-plus-control rows on one control measure, and a settings × channels switch matrix.

Status
stable
Since
0.1.0
Accessibility pattern
headings + labelled rows

Last updated

Email notificationsProduct updates, tips, and offers.
ThemeHow the interface looks on this device.
Two-factor authenticationRequire a code at sign-in.

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-row

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/settings-row

Run 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):

SettingsCard — data-slot="settings-card"
SettingsRow — data-slot="settings-row" | "settings-row-control" | "settings-row-description" | "settings-row-label"
SettingsSection — data-slot="settings-section" | "settings-section-actions" | "settings-section-description" | "settings-section-header" | "settings-section-title"
SettingsMatrix — data-slot="settings-matrix" | "settings-matrix-cell" | "settings-matrix-cell-label" | "settings-matrix-cells" | "settings-matrix-header" | "settings-matrix-label" | "settings-matrix-row"

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 optional title and description above its children, with optional actions ("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's Card (data-slot="settings-card"), flattened for a divided list: py-0 gap-0, so rows sit edge to edge and each row's own border-b is the only divider. The radius, the bg-card ground and the border border-border hairline come from Card, 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"): a label and optional description on the left, the control children on the right, with a border-b divider. 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; no SettingsCard required.

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:

Email notificationsProduct updates, tips, and offers.
ThemeHow the interface looks on this device.
Two-factor authenticationRequire a code at sign-in.

A full section with a titled header above a card of switch rows:

Notifications

Choose what you want to hear about.

EmailProduct updates and tips.
SMSCritical alerts only.
Desktop pushReal-time alerts on this device.

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.

Shown across the product.
Where customer replies are sent.

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.

Time zone
Prefixes every task key.
Daily digest

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.

Assigned to me
MentionsWhen someone @mentions you.
Comments and replies
Due soonA day before a due date.

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.

Email notificationsProduct updates and tips.
Workspace IDUsed in API requests.
ws_8f3a1c
PlanYour current subscription tier.
Pro
Active sessionsSign out everywhere else.
Danger zoneDeleting the workspace is permanent and cannot be undone.

API Reference

SettingsRow

PropTypeDefaultDescription
label*React.ReactNode—The row label rendered on the left (heading line of the row).
childrenReact.ReactNode—The control rendered on the right — a Switch, Input, Button, badge, or read-only value.
controlIdstring—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.
descriptionReact.ReactNode—Optional supporting description rendered under the label (muted).
labelPropsReact.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>—Props merged onto the generated label element when controlId is provided.

Data attributes and CSS variables on SettingsRow

AttributeValues
data-control-sizemirrors a prop or state value
data-slot"settings-row" | "settings-row-control" | "settings-row-description" | "settings-row-label"

SettingsMatrix

PropTypeDefaultDescription
columns*readonly React.ReactNode[]—The column names ("Push", "Email"), shown once above the switches.
rows*readonly SettingsMatrixRow[]—The settings, one row each.
labelReact.ReactNode—The header above the row labels ("What you hear about"). Omit for an empty corner.

Data attributes and CSS variables on SettingsMatrix

AttributeValues
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-columnsCSS custom property

Each row of rows:

PropTypeDefaultDescription
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.
descriptionReact.ReactNode—A muted line under the label.
disabledboolean | readonly boolean[]falseSwitches that cannot change — true for the whole row, or one flag per column.
idstringthe row's indexA 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

PropTypeDefaultDescription
size"default" | "sm"—
variant"default" | "outline"—

Data attributes and CSS variables on SettingsCard

AttributeValues
data-slot"settings-card"

SettingsSection

PropTypeDefaultDescription
actionsReact.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.
descriptionReact.ReactNode—Supporting description rendered under the title (muted).
titleReact.ReactNode—Section heading rendered above the grouped content.
titleAsSettingsSectionTitleTag'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

AttributeValues
data-slot"settings-section" | "settings-section-actions" | "settings-section-description" | "settings-section-header" | "settings-section-title"

Accessibility

  • SettingsSection renders its title as a real heading — <h3> by default, or whatever titleAs names (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 controlId when the row contains a form control with a matching id; the row label becomes a real <label htmlFor={controlId}>. For composite controls that do not expose an input id, keep an explicit aria-label or aria-labelledby on the control itself.
  • Every SettingsMatrix switch is named by its row and its column ("Mentions Email") through aria-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-border rather than by elevation, so the divided list stays legible in high-contrast and forced-colors modes; the card's own hairline is Card's border border-border (BRD-1).
KeyAction
TabMove focus through the controls in each row, top to bottom.
Enter / SpaceActivate or toggle the focused control.
ContractStates tested
Behaviourdefault, read-only
Accessibilitylabeled, semantic-html
Visualdefault

Do / Don't

Do
Pair each row with a labelled control, using controlId for native inputs and explicit ARIA labels for composite controls.
Don't
Stack bare rows without a card, or rely on visual proximity alone to name an interactive control.

On this page