Settings page
The page nobody designs is where trust gets built
## The starting point
```tsx
<div className="p-6">
<h1 className="text-xl font-bold">Settings</h1>
<div className="mt-6 border-b pb-6">
<h3 className="font-bold">Profile</h3>
<div className="mt-4 space-y-4">
<input placeholder="Name" className="w-full rounded border p-2" />
<input placeholder="Email" className="w-full rounded border p-2" />
<input placeholder="Website" className="w-full rounded border p-2" />
</div>
</div>
<div className="mt-6 border-b pb-6">
<h3 className="font-bold">Notification preferences</h3>
<label className="flex items-center gap-2">
<input type="checkbox" /> Email notifications
</label>
<label className="flex items-center gap-2">
<input type="checkbox" /> Marketing emails
</label>
</div>
<div className="mt-6">
<h3 className="font-bold text-red-500">Danger zone</h3>
<button className="mt-4 rounded bg-red-500 px-4 py-2 text-white">
Delete Account
</button>
</div>
</div>
```
Count the problems.
## Layer 1: Copywriting
### Section headings
**Before:** "Profile" | **After:** "Public profile". Now the user knows _who sees this data_.
**Before:** "Notification preferences" | **After:** "What we send you".
**Before:** "Danger zone" | **After:** "Delete your account". Name the action, skip the theatrics.
### Toggle labels
**Before:** "Email notifications" | **After:** "Send me email when someone replies"
**Before:** "Marketing emails" | **After:** "Send me product updates and tips"
### Delete button
**Before:** "Delete Account" | **After:** "Permanently delete my account and all data"
### Helper text
Every field should say why it collects data. Below email: "We'll use this for sign-in and account recovery." Below website: "Shown on your public profile."
## Layer 2: Typography
### Heading hierarchy
The original jumps from `h1` to `h3`.
```tsx
<h1 className="text-2xl font-semibold tracking-tight">Settings</h1>
<h2 className="text-sm font-medium tracking-widest uppercase text-muted-foreground">
Public profile
</h2>
```
Section headers are small and uppercase wayfinding labels. Field labels use `text-sm font-medium`. Hierarchy from treatment, not raw pixels.
### Separator audit
Replace `border-b` on every section with `space-y-10`. Save the single `border-t` for the destructive action. That separation is load-bearing.
## Layer 3: Craft
### Labels and autocomplete
```tsx
<label htmlFor="email" className="text-sm font-medium">Email address</label>
<input id="email" name="email" type="email" inputMode="email" autoComplete="email" />
```
### Inline validation
```tsx
<input
type="email"
onBlur={(e) => {
if (!e.target.validity.valid) setEmailError("Enter a valid email address");
}}
aria-describedby={emailError ? "email-error" : undefined}
aria-invalid={!!emailError}
/>;
{
emailError && (
<p id="email-error" className="mt-1 text-sm text-destructive">
{emailError}
</p>
);
}
```
### Save button
```tsx
<button
type="submit"
disabled={isPending}
className="inline-flex h-9 items-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground disabled:opacity-50"
>
{isPending && <Spinner className="size-4 animate-spin" />}
Save changes
</button>
```
Never swap the label to "Saving..." The user shouldn't wonder which button they pressed.
### Destructive action
```tsx
<section className="mt-10 border-t pt-10">
<h2 className="text-sm font-medium tracking-widest uppercase text-destructive">
Delete your account
</h2>
<p className="mt-2 text-sm text-muted-foreground">
This permanently deletes your account, profile, and all data. This cannot be
undone.
</p>
<button
onClick={() => setShowConfirm(true)}
className="mt-4 h-9 rounded-md bg-destructive px-4 text-sm font-medium text-destructive-foreground"
>
Permanently delete my account and all data
</button>
</section>
```
## Layer 4: Animation
### Save feedback
```tsx
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
<Check className="size-4 text-emerald-500" />
</motion.div>
```
### Toggle animation
```tsx
<motion.span
className="block size-4 rounded-full bg-white shadow-sm"
animate={{ x: isOn ? 18 : 0 }}
transition={{ type: "spring", stiffness: 500, damping: 35 }}
/>
```
### Async loading
```tsx
<motion.section
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
/>
```
### Reduced motion
```tsx
const prefersReduced = useReducedMotion();
<motion.span
animate={{ x: isOn ? 18 : 0 }}
transition={
prefersReduced
? { duration: 0 }
: { type: "spring", stiffness: 500, damping: 35 }
}
/>;
```
The toggle still moves (it communicates state) but instantly.