Settings
A settings page in five sections (profile, account, notifications, appearance, danger zone) with an unsaved changes bar and two dialogs, made of the Rabtx components. Every animation is CSS.
Settings
Manage your profile, account and preferences.
Profile
How others see you.
Lowercase letters, numbers and _.
Up to 160 characters.
Account
Your sign-in email, plan and region.
Language and region
Used for dates, times and the interface.
Notifications
Pushed to your devices as they happen.
Email digests
Sent to your sign-in email.
Theme
System follows your device.
Density
How tightly lists and tables are packed.
Delete account
Permanently remove your account and everything in it. This cannot be undone.
Installation
Add the registry to components.json, once
{
"registries": {
"@rabtx": "http://localhost:3002/r/{name}.json"
}
}Add the component
npx shadcn@latest add @rabtx/settings-blockOr, without the namespace, by URL:
npx shadcn@latest add http://localhost:3002/r/settings-block.jsonThe block lands in components/blocks and the components it uses in your components/ui, styled by your own theme. Add --overwrite to replace a shadcn component with the same name.
Usage
Installed from the registry, import from @/components/blocks/settings/settings-block; inside
this monorepo, from @rabtx/ui/blocks/settings.
import { SettingsBlock } from "@rabtx/ui/blocks/settings";
export function SettingsPage({ user }) {
return (
<SettingsBlock
email={user.email}
plan={user.plan}
defaultValues={user.settings}
onSave={async (values, { changed }) => {
const res = await fetch("/api/settings", { method: "PATCH", body: JSON.stringify(values) });
if (res.status === 409) return { username: "That username is taken." };
if (!res.ok) throw new Error("We could not save your settings. Try again.");
}}
onChangeEmail={async (email) => {
const res = await fetch("/api/email", { method: "POST", body: JSON.stringify({ email }) });
if (!res.ok) return "That email is already in use.";
}}
onDeleteAccount={async () => {
await fetch("/api/account", { method: "DELETE" });
location.assign("/goodbye");
}}
/>
);
}The sections are Tabs, each panel a stack of Cards:
- Profile: an avatar with the name's initials (or
avatarUrl), the plan Badge, then Field + Input for the display name, the username and the website, and a Textarea for the bio. - Account: the sign-in email with a Change email Dialog, the plan, and Selects for the language and the time zone.
- Notifications: Switch rows for pushes, and Checkboxes for the email digests.
- Appearance: RadioGroup choice cards for the theme (light, dark, system), each with a small drawing of it, and for the density.
- Danger zone: Delete account opens a Dialog that asks for a typed phrase.
Change anything and a bar slides in at the bottom with the count, Discard and Save
changes (a StatefulButton). Ctrl+S or ⌘S saves from anywhere in the block. Without
onSave, onChangeEmail and onDeleteAccount the block only waits a moment, so it can be tried
as is. There is no fake network and nothing is stored.
Narrow
The layout follows the block's own width, not the window's: it is a size container, and below 40rem the section list is a row of tabs that scrolls sideways above the panel. The tabs' arrow keys follow (left and right on top, up and down beside).
Preferences
A narrow column: the tabs sit on top.
Profile
How others see you.
Lowercase letters, numbers and _.
Up to 160 characters.
Account
Your sign-in email, plan and region.
Language and region
Used for dates, times and the interface.
Notifications
Pushed to your devices as they happen.
Email digests
Sent to your sign-in email.
Theme
System follows your device.
Density
How tightly lists and tables are packed.
Delete account
Permanently remove your account and everything in it. This cannot be undone.
Validation
The saved controls belong to one native <form> through their form attribute, so the dialogs,
which have forms of their own, can sit inside the cards. The rules are the browser's constraints:
required and a pattern on the name so spaces alone do not count, pattern="[a-z0-9_]{3,20}"
on the username, type="url" on the website and maxLength on the bio. The block reads
ValidityState and shows a message through each control's Field.
- A filled field is checked when focus leaves it; an empty one only on Save. An error clears the moment the field is fixed.
- Save checks every section. The first failing field's section opens and the field takes focus; a red dot marks every tab with an error.
onSavereceives the values and the changed field names. Return field errors ({ username: "…" }) to show them the same way, or throw to show the message on the bar.- Change email rejects a malformed address and your current one;
onChangeEmailcan return a message. Delete account needs the phrase typed exactly: it is the field'spattern.
API
Prop
Type
SettingsValues is { name, username, website, bio, language, timezone, mentions, comments, security, dailyDigest, weeklyDigest, productNews, theme, density }; SETTINGS_DEFAULTS has the
defaults. SettingsSection is "profile" | "account" | "notifications" | "appearance" | "danger".
ChangeEmailDialog and DeleteAccountDialog are exported for use on their own.
Theming
The block reads only the theme tokens, through its components and
settings-block.css.
| Part | Tokens |
|---|---|
| Avatar, unsaved dot, chosen density | --primary, --card |
| Danger zone, delete buttons, error dot | --destructive, --primary-foreground, --border |
| Bar | --popover / --popover-foreground, --border; --success once saved |
| Drawings | --background, --foreground, --muted, --muted-foreground, --primary in the rabtx light and rabtx dark scopes |
| Motion | --spring-panel, --spring-swap, --motion-ease |
How the motion works
| Effect | CSS |
|---|---|
| Layout | container: rx-settings / inline-size; @container rx-settings (width >= 40rem) turns the tab row into a sticky column beside the panels |
| Section | the Tabs indicator glides on its spring; after the first change (data-moved) the new panel's cards rise 0.75rem and fade in from @starting-style, 60ms apart (--_i) |
| Bar in | .rx-settings:has(> form[data-dirty]) > .rx-settings-bar turns display: flex; @starting-style gives the first frame, 1rem lower, faded and at 0.98, and it springs up on panel |
| Bar out | display transitions with allow-discrete, so the bar fades down before it leaves the layout, the tab order and the accessibility tree |
| Tab dots | a tab whose section has changes (data-dirty) or errors (data-invalid) scales a dot in on the swap spring |
| Density | the drawing's rows close up on the swap spring, and the first takes --primary when chosen |
Reduced motion and data-motion="off" stop every rise, slide and scale (the bar only fades with
reduced motion); every state still shows.
Accessibility
- The sections are WAI-ARIA tabs;
aria-orientationand the arrow keys follow the layout the container query picked. Each panel is a tab stop and holds Cards named by their headings. - Every control is named and described through
Field, or by its row's text (the Switches) and its group's heading (the Checkboxes and RadioGroups). Errors are announced. - A polite live region says when there are unsaved changes, when they are saved or discarded and which section needs attention. Tabs with changes or errors say so to screen readers too.
- The bar is a region named "Unsaved changes". When it leaves after a save or a discard, focus
moves to the open panel instead of being lost. The Save button carries
aria-keyshortcuts. - The dialogs are native modal
<dialog>s named by their titles; focus moves in and returns to the trigger. While they work, Escape and the backdrop do not close them.