Skip to main content

Settings Row

The row every settings page repeats, in one piece. Label and description on the left, the control on the right, with a section that stacks them and draws the rules.

Notifications

How and when NachUI reaches you.

One email for every event on your account.

Beta

How often the alerts are bundled.

Shown on every email we send.

1'use client';
2
3import { useState } from 'react';
4import { Badge } from '@/components/ui/badge';
5import { Button } from '@/components/ui/button';
6import { SettingsRow } from '@/components/ui/settings-row';
7
8const FREQUENCIES = [
9 { value: 'instant', label: 'Instantly' },
10 { value: 'hourly', label: 'Hourly' },
11 { value: 'daily', label: 'Daily digest' },
12];
13
14export function Default() {
15 const [email, setEmail] = useState(true);
16 const [frequency, setFrequency] = useState('daily');
17 const [replyTo, setReplyTo] = useState('nacho@example.com');
18
19 return (
20 <div className="w-full max-w-xl">
21 <SettingsRow.Section
22 title="Notifications"
23 description="How and when NachUI reaches you."
24 footer={
25 <Button size="sm" type="button">
26 Save changes
27 </Button>
28 }
29 >
30 <SettingsRow label="Email alerts" description="One email for every event on your account.">
31 <SettingsRow.Switch checked={email} onCheckedChange={setEmail} />
32 </SettingsRow>
33 <SettingsRow
34 label="Frequency"
35 description="How often the alerts are bundled."
36 badge={<Badge variant="secondary">Beta</Badge>}
37 disabled={!email}
38 >
39 <SettingsRow.Select
40 options={FREQUENCIES}
41 value={frequency}
42 onValueChange={setFrequency}
43 />
44 </SettingsRow>
45 <SettingsRow label="Reply-to address" description="Shown on every email we send.">
46 <SettingsRow.Input
47 type="email"
48 value={replyTo}
49 onChange={(event) => setReplyTo(event.target.value)}
50 />
51 </SettingsRow>
52 </SettingsRow.Section>
53 </div>
54 );
55}

Installation

pnpm dlx nachui add settings-row

Anatomy

1import { SettingsRow } from '@/components/ui/settings-row';
1<SettingsRow.Section
2 title="Notifications"
3 description="How we reach you."
4 footer={<Button size="sm">Save</Button>}
5>
6 <SettingsRow label="Email alerts" description="One email per event.">
7 <SettingsRow.Switch checked={email} onCheckedChange={setEmail} />
8 </SettingsRow>
9 <SettingsRow label="Frequency" badge={<Badge variant="secondary">Beta</Badge>}>
10 <SettingsRow.Select options={frequencies} value={frequency} onValueChange={setFrequency} />
11 </SettingsRow>
12 <SettingsRow label="Reply-to address">
13 <SettingsRow.Input type="email" value={replyTo} onChange={onReplyTo} />
14 </SettingsRow>
15</SettingsRow.Section>
A row takes a label, an optional description and one control as its child. It generates the id, points the Label at it and hands the description to the control as aria-describedby, so a screen reader hears the same thing a sighted reader sees. The row stacks on a phone and goes side by side from sm up.
Any element works as the child. The built-in parts are the same elements, already sized for the row and already wired to its label.

Composition

A hybrid is visible composition. Open settings-row.tsx and these are the elements it is made of, in this order:
SettingsRow
├── Label (+ Badge when given)
├── description (plain text, aria-describedby)
└── control
├── SettingsRow.Switch → Switch
├── SettingsRow.Select → Select.Trigger + Select.Content + Select.Item
├── SettingsRow.Input → Input
├── SettingsRow.Action → Button
└── SettingsRow.Value → <code> + copy button
SettingsRow.Section
├── header (title, description)
├── rows separated by Separator
└── footer (slot)

Switch

SettingsRow.Switch is the Switch element with the row's id on its input. Clicking the label toggles it. checked and onCheckedChange work as on the element, and disabled on the row disables it.

Select

SettingsRow.Select takes options as { value, label } and renders a small Select on the right. Its trigger is labelled by the row, so the combobox reads with the row's name. It never stores the value: pass value and onValueChange, or defaultValue to leave it uncontrolled.

Input

SettingsRow.Input is the Input element at the small size, capped at a sensible width so a long row does not stretch it. Every input prop passes through except id, label and description, which the row owns.

Action

SettingsRow.Action is a Button, outline and small by default. Use it for "Manage", "Rotate" or, with variant="destructive", for the one thing on the page that cannot be undone.

Value

SettingsRow.Value shows a read-only value in mono, an API key prefix or an account id. Set copy to add a copy button that writes the value to the clipboard and swaps to a check for a moment.

Danger zone

Keys and actions that are hard to undo.

Use it from the server only. Rotates on demand.

sk_live_4f2a…9c1e

The current key stops working right away.

Removes the workspace, its keys and every component synced to it.

1'use client';
2
3import { useState } from 'react';
4import { SettingsRow } from '@/components/ui/settings-row';
5
6export function Danger() {
7 const [deleted, setDeleted] = useState(false);
8
9 return (
10 <div className="w-full max-w-xl">
11 <SettingsRow.Section
12 title="Danger zone"
13 description="Keys and actions that are hard to undo."
14 className="border-destructive-border"
15 >
16 <SettingsRow label="API key" description="Use it from the server only. Rotates on demand.">
17 <SettingsRow.Value value="sk_live_4f2a…9c1e" copy />
18 </SettingsRow>
19 <SettingsRow label="Rotate key" description="The current key stops working right away.">
20 <SettingsRow.Action>Rotate</SettingsRow.Action>
21 </SettingsRow>
22 <SettingsRow
23 label="Delete account"
24 description="Removes the workspace, its keys and every component synced to it."
25 >
26 <SettingsRow.Action variant="destructive" onClick={() => setDeleted(true)}>
27 {deleted ? 'Scheduled' : 'Delete account'}
28 </SettingsRow.Action>
29 </SettingsRow>
30 </SettingsRow.Section>
31 </div>
32 );
33}

Section

SettingsRow.Section groups rows under a title in a bordered card. It puts a Separator between every pair of rows and takes a footer slot for a save button or a note. Disabling a row dims it; disabling the whole section is a matter of passing disabled to each row you want off.

API Reference

SettingsRow

PropTypeDefaultDescription
labelReactNode-The row's label, rendered with Label
descriptionReactNode-Helper text under the label, linked to the control
htmlForstring-Id for the control; generated when left out
badgeReactNode-Rendered next to the label, usually a Badge
disabledbooleanfalseDims the row and disables the built-in control
childrenReactNode-The control, a built-in part or any element of your own
classNamestring-Additional CSS classes

SettingsRow.Switch

PropTypeDefaultDescription
checkedboolean-Controlled state
defaultCheckedboolean-Initial state
onCheckedChange(checked: boolean) => void-Called on every toggle
disabledbooleanrow'sOverrides the row's value
classNamestring-Additional CSS classes

SettingsRow.Select

PropTypeDefaultDescription
options{ value: string; label: string; disabled?: boolean }[]-The entries of the list
valuestring-Controlled value
defaultValuestring-Initial value when uncontrolled
onValueChange(value: string) => void-Called with the picked value
placeholderstring-Shown while nothing is selected
namestring-Form field name
disabledbooleanrow'sOverrides the row's value
classNamestring-Additional CSS classes

SettingsRow.Input

PropTypeDefaultDescription
size'sm' | 'default' | 'lg''sm'Input size
disabledbooleanrow'sOverrides the row's value
classNamestring-Additional CSS classes
...--Any input prop except id, label, description

SettingsRow.Action

PropTypeDefaultDescription
variant'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link''outline'Button variant
size'default' | 'sm' | 'lg' | 'icon''sm'Button size
disabledbooleanrow'sOverrides the row's value
...--Any Button prop

SettingsRow.Value

PropTypeDefaultDescription
valuestring-The text shown in mono
copybooleanfalseAdds the copy button
copyLabelstring'Copy'Accessible name at rest
copiedLabelstring'Copied'Accessible name for 1.5s after copying
onCopied(value: string) => void-Called after the clipboard write
classNamestring-Additional CSS classes

SettingsRow.Section

PropTypeDefaultDescription
titleReactNode-Heading of the card
descriptionReactNode-Text under the heading
footerReactNode-Slot at the bottom, right aligned
childrenReactNode-Rows, separated by a Separator each
classNamestring-Additional CSS classes
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue