4.1 KiB
4.1 KiB
Good Example: React Component
A UI component that follows Atelier's UI/UX principles. Each aspect cites the principle it satisfies.
The Component
import { useId, useState } from 'react';
import { Button } from './Button';
import { Spinner } from './Spinner';
type DeleteButtonProps = {
/** The resource name to display in the confirmation */
resourceName: string;
/** Called when the user confirms deletion */
onDelete: () => Promise<void>;
};
export function DeleteButton({ resourceName, onDelete }: DeleteButtonProps) {
const [isConfirming, setIsConfirming] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const confirmId = useId();
const handleConfirm = async () => {
setIsDeleting(true);
try {
await onDelete();
} finally {
setIsDeleting(false);
setIsConfirming(false);
}
};
if (isConfirming) {
return (
<span role="group" aria-labelledby={confirmId}>
<span id={confirmId}>Delete {resourceName}? This cannot be undone.</span>
<Button variant="danger" onClick={handleConfirm} disabled={isDeleting}>
{isDeleting ? <Spinner label="Deleting" /> : 'Yes, delete'}
</Button>
<Button variant="ghost" onClick={() => setIsConfirming(false)} disabled={isDeleting}>
Cancel
</Button>
</span>
);
}
return (
<Button variant="danger" onClick={() => setIsConfirming(true)}>
Delete
</Button>
);
}
What Makes It Good
Single Responsibility (components.md §1)
- The component does one thing: confirm and trigger a deletion. No "And" in the name.
- The
onDeletecallback is the single output. The component owns the confirmation UI, not the deletion logic.
Explicit Boundaries (components.md §3)
- Props are typed (
DeleteButtonProps). Required props are required. onDeletereturns aPromise<void>— the caller knows it's async.- The component never reads global state. It receives
resourceNameandonDelete.
Predictable State (components.md §4)
isConfirmingandisDeletingare owned by the component (only it cares).- State is not duplicated. The parent does not know about confirmation.
- The component transitions: idle → confirming → deleting → idle.
Render Purity (components.md §5)
- Given the same props and state, the component renders the same output.
- Side effects (
onDelete) are in the event handler, not in render. useId()is deterministic per component instance (React guarantee).
Accessible by Default (components.md §6, uiux P2)
- The confirmation group has
role="group"andaria-labelledby. - The Spinner has a
label(screen reader announces "Deleting"). - Buttons have text labels (not icon-only).
- Focus order is logical (confirm → cancel).
- Keyboard-reachable (buttons are natively focusable).
Forgiveness (UI/UX P5, P10 Reversibility)
- Destructive action requires confirmation (P5).
- "This cannot be undone" names the consequence (P3 Clarity).
- "Cancel" is offered and is not disabled during deletion (the user can cancel the next action).
- The state is reversible:
isConfirmingcan be set back tofalse(P10).
Style via Tokens (components.md §7)
variant="danger"andvariant="ghost"reference design tokens, not raw colors.- No
style={{ color: 'red' }}— the token system owns the visual.
Feedback (UI/UX P4)
- The button shows a Spinner while deleting (P4, P6 Performance perception).
- The button is disabled while deleting (prevents double-click).
- The label changes: "Yes, delete" → Spinner (state is communicated).
Clarity (UI/UX P3)
- "Delete {resourceName}? This cannot be undone." — specific, names the resource and the consequence.
- No "Are you sure?" — vague. No "Submit" — wrong verb.
What This Example Does NOT Do (And Why That's Good)
- Does not use a
window.confirm()dialog — not accessible, not styled, not composable. - Does not render a modal — the inline confirmation is lighter and less disruptive (P9 Simplicity).
- Does not auto-delete on click — forgiveness (P5).
- Does not hardcode "Project" — the resource name is a prop (composability, components.md §3).