Settings pages
A settings page is a SidebarNav of categories and a column of sections. Each section has a title and one sentence on the left, controls on the right, and collapses to one column on narrow screens.
<form onChange={() => setDirty(true)} onSubmit={save}>
<Stack gap={8}>
<Section title="Profile" description="Shown on comments and in the member list.">
<Field label="Display name"><Input name="name" defaultValue="Ada Lovelace" /></Field>
<Field label="Time zone"><NativeSelect name="tz">…</NativeSelect></Field>
</Section>
<Separator />
<Section title="Notifications" description="Email is sent at most once an hour.">
<Switch name="mentions" defaultChecked>Mentions and replies</Switch>
</Section>
<Separator />
<Stack direction="row" justify="end" gap={2} align="center">
{dirty ? <Text size="sm" tone="muted">Unsaved changes</Text> : null}
<Button type="submit" variant="primary" disabled={!dirty} loading={saving}>Save changes</Button>
</Stack>
</Stack>
</form>Save model
Pick one per page and stick to it:
- Explicit save (shown above) for text fields and anything that affects other people: a single Save button at the end, disabled until something changes, with "Unsaved changes" next to it. Confirm with a toast.
- Instant apply for pages that are only toggles (notification preferences, feature flags): each
Switchsaves on change and shows a toast on failure, reverting the switch. No Save button.
Never mix both in one section: users cannot tell which changes are saved.
Structure
- Group by what users look for, not by data model: Account, Workspace, Billing, Security.
SidebarNavmarks the current category withactive, which setsaria-current="page". Each category is its own route.- Sections separated by
Separator, titles withHeading size="sm", descriptions in muted text. - Keep labels short and put the explanation in
Field'sdescriptionor the Switch's label.
Danger zone
Destructive account actions go last, in their own outlined card, with a danger button that opens a confirmation — see Confirmations.
Delete account
Removes your profile and leaves every workspace.
<Card variant="outline" padding="md">
<Stack direction="row" gap={4} justify="between" align="center" wrap>
<Stack gap={1}>
<Text weight="medium" tone="ink" size="sm">Delete account</Text>
<Text size="sm" tone="muted">Removes your profile and leaves every workspace.</Text>
</Stack>
<Button variant="danger" size="sm">Delete account</Button>
</Stack>
</Card>