Preview
Settings pattern
Group related settings, make unsaved changes visible, and keep saving feedback next to the action that caused it.
Workspace settings with changed, saving, success, and error states
"use client";
import { useState } from "react";
import { Button } from "@phuctech/ui/components/button";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@phuctech/ui/components/card";
import { Field, FieldLabel } from "@phuctech/ui/components/field";
import { Input } from "@phuctech/ui/components/input";
export function WorkspaceSettings() {
const [name, setName] = useState("PhucTech");
const [savedName, setSavedName] = useState("PhucTech");
const changed = name !== savedName;
return (
<Card>
<form onSubmit={(event) => { event.preventDefault(); setSavedName(name); }}>
<CardHeader><CardTitle>Workspace settings</CardTitle></CardHeader>
<CardContent>
<Field>
<FieldLabel htmlFor="workspace-name">Workspace name</FieldLabel>
<Input id="workspace-name" value={name} onChange={(event) => setName(event.target.value)} />
</Field>
</CardContent>
<CardFooter>
<Button type="submit" disabled={!changed}>Save changes</Button>
<Button type="button" variant="outline" disabled={!changed} onClick={() => setName(savedName)}>Discard</Button>
</CardFooter>
</form>
</Card>
);
}When to use
Use Settings when a person edits durable preferences rather than a one-off record. Give each group a short heading and description so a long page remains scannable.
States
Layout
- Keep the section heading, explanation, fields, and actions in one surface when they belong to the same decision.
- Disable Save only when there are no changes or a save is already in flight; do not disable it merely because a field is optional.
- Discard restores the last saved snapshot, not an empty default.
- Use a page for settings that need navigation or a multi-step workflow; do not force a large settings area into a Dialog.
"use client";
import { useState } from "react";
import { Button } from "@phuctech/ui/components/button";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@phuctech/ui/components/card";
import { Field, FieldLabel } from "@phuctech/ui/components/field";
import { Input } from "@phuctech/ui/components/input";
export function WorkspaceSettings() {
const [name, setName] = useState("PhucTech");
const [savedName, setSavedName] = useState("PhucTech");
const changed = name !== savedName;
return (
<Card>
<form onSubmit={(event) => { event.preventDefault(); setSavedName(name); }}>
<CardHeader><CardTitle>Workspace settings</CardTitle></CardHeader>
<CardContent>
<Field>
<FieldLabel htmlFor="workspace-name">Workspace name</FieldLabel>
<Input id="workspace-name" value={name} onChange={(event) => setName(event.target.value)} />
</Field>
</CardContent>
<CardFooter>
<Button type="submit" disabled={!changed}>Save changes</Button>
<Button type="button" variant="outline" disabled={!changed} onClick={() => setName(savedName)}>Discard</Button>
</CardFooter>
</form>
</Card>
);
}Accessibility
- Use headings for groups and keep their descriptions adjacent to the controls they explain.
- Keep status feedback in an
aria-live="polite"region so it is announced without interrupting editing. - Do not communicate unsaved, error, or success state by color alone; include text.
- Preserve focus after saving and after validation so keyboard users do not lose their place.
Do and don’t
Do
Show the changed state as soon as a value differs from the saved snapshot and make recovery obvious.
Don’t
Auto-save every keystroke for settings with consequential effects or silently discard edits on navigation.