
Ux Enhancer
- 25 installs
- 4 repo stars
- Updated May 8, 2026
- gashiartim/ux-enhancer
Refactor an existing React component for usability using Steve Krug principles: visual hierarchy, reduced copy, unambiguous CTAs, and explicit loading, empty, and error states.
About
Audits and refactors React components to cut cognitive load by fixing hierarchy, trimming copy, clarifying CTAs, and adding missing states. A developer uses it when they share a component and want a usability audit or a cleaner, more intuitive interface.
- Applies Steve Krug's Don't Make Me Think principles to existing components
- Refactors in a set order: cut copy, fix hierarchy, fix states, fix interactions
Ux Enhancer by the numbers
- 25 all-time installs (skills.sh)
- Ranked #1,346 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gashiartim/ux-enhancer --skill ux-enhancerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25 |
|---|---|
| repo stars | ★ 4 |
| Last updated | May 8, 2026 |
| Repository | gashiartim/ux-enhancer ↗ |
What it does
Refactor an existing React component for usability using Steve Krug principles: visual hierarchy, reduced copy, unambiguous CTAs, and explicit loading, empty, and error states.
Files
UX Enhancer
A UX refactor specialist for React components. The job is to remove cognitive friction — not to redesign the feature, not to add abstractions, not to "improve" the architecture.
North star
Steve Krug's first law: Don't make me think. A first-time user should navigate the screen correctly with zero training. If they have to pause, re-read, or hunt for the primary action, the design failed.
Three operating principles, in priority order:
1. Users scan, they don't read. Optimize for the 3-second glance. 2. Users satisfice. They pick the first plausible option, not the best one. Make the right choice the most prominent one. 3. Users muddle through. They don't understand your model. Conventions over cleverness.
When to use this skill
Use it when:
- A React component shared with intent to improve usability, copy, hierarchy, navigation, forms, or states.
- A component has instruction paragraphs, ambiguous CTAs, dense walls of text, or missing empty/loading/error states.
- The user mentions cognitive load, scannability, "Don't Make Me Think," or asks "why is this confusing?"
Skip it when:
- Backend / API / DB / business logic only — no UI to refactor.
- Pure performance work (memoization, bundle size) — use
vercel-react-best-practices. - Visual branding, animation polish, or building a landing page from scratch — use
frontend-designorimpeccable. - The user wants a fresh design from a brief — this skill refactors existing UI.
Workflow
Run these steps in order. Don't skip steps.
1. Understand the user's task
Read the component. Answer in your head:
- What screen is this? (Settings? List? Form? Modal?)
- Who is the user? (Admin? Customer? Internal staff?)
- What is the user trying to do here?
If this isn't obvious from the code, ask one clarifying question before refactoring.
2. Identify the primary action
Every screen has one most-important thing the user came to do. Find it. The refactor must make that action the most prominent visual element on the screen.
If you can't identify a primary action, the screen is doing too much — flag it as a structural issue rather than refactoring noise.
3. Audit cognitive friction
Walk the red flags checklist (below) AND the smell catalog at references/component-smell-catalog.md. Note every hit. Don't fix yet — just inventory.
Triage rules:
- Sort hits by severity: Blocker → High → Medium → Low.
- Fix Blockers and High first. Do not refactor cosmetic / Low issues while a Blocker exists.
- Tie every major change in the output bullets to a specific smell name (e.g. "Vague button smell") or a Krug principle.
4. Refactor in this order
1. Cut copy — happy talk, instructions, verbose labels. Half the wins live here. 2. Fix hierarchy — promote primary, demote secondaries, group related, separate unrelated. 3. Fix states — loading, empty, error, success, disabled, pending all need explicit handling. 4. Fix interactions — clickability, current state indicators, hover-only behaviors. 5. Tighten labels and microcopy — verb of what happens, not the form mechanic.
5. Preserve the existing design system
Detect the project's DS (shadcn/ui, MUI, Chakra, Mantine, Ant Design, or custom) by scanning imports. Use what exists. Do not invent new primitives. If no fit, flag with // New pattern — DS gap.
6. Explain changes
Output the refactored code, then a 3–7 bullet list mapping each significant change to its cognitive-load reason or Krug principle. Specific. Tied to the code. No generic praise.
Krug rules → React translations
| Krug rule | React translation |
|---|---|
| Pages should be self-evident | Title, primary action, and content type readable in 3 seconds without scrolling |
| Users scan, they don't read | Headings + lists > paragraphs. Equal-weight labels are a smell. |
| Conventions over cleverness | Top-left logo links home. Hamburger = nav. Magnifier = search. Don't reinvent. |
| Visual hierarchy beats decoration | Size, weight, spacing communicate importance — not gradients or icons |
| Clickable looks clickable | Buttons are buttons. Not text with cursor: pointer and a hover. |
| Cut needless words ruthlessly | Get rid of half. Then half again. |
| Kill happy talk | "Welcome to your dashboard!" → delete entirely |
| Navigation answers Where am I / What can I do / Where can I go | Active page indicator, breadcrumb when ≥2 levels deep, persistent main nav |
| Page titles match clicked links | If the user clicked "Patients," the page H1 says Patients, not Patient management |
| Forms are forgiving and obvious | Labels above fields, inline validation, retain user input on error |
| Empty/loading/error guide next action | Never blank, never silent, never dead-end |
| Mobile: no hover-only, big tap targets | 44×44px min, all hover affordances also tappable |
| Accessibility is part of usability | Semantic HTML, labeled controls, focus visible, color not the only signal |
| Don't waste user time | One click better than two. One word better than two. |
Red flags checklist
Inventory all hits before refactoring.
Copy smells
- [ ] "Welcome to..." or other happy talk paragraphs
- [ ] Instruction paragraph above a form
- [ ] Field labels longer than 3 words
- [ ] Buttons labeled
Submit,OK,Confirm,Click here - [ ] Sentence-form labels: "Please enter your..."
- [ ] Duplicate copy: tab label = page heading = card title
- [ ] Marketing voice in product UI ("amazing," "powerful," "simply")
Hierarchy smells
- [ ] Multiple H1s, or no H1
- [ ] Primary CTA visually equal to secondaries
- [ ] All buttons same color/weight
- [ ] Decorative emphasis (gradients, glows) competing with real CTAs
- [ ] Sidebar with 15+ flat items, no grouping
Interaction smells
- [ ] Text styled to look like a link, but isn't (or vice versa)
- [ ]
cursor: pointeron a non-button div - [ ] Hover-only menus, tooltips with critical info, hover-only edit buttons
- [ ] Modal with X-only dismiss (no Cancel + primary)
- [ ] Destructive action (Delete, Remove) styled identically to safe ones
- [ ] No focus ring / focus invisible
State smells
- [ ] Inline
<Spinner />with no context label - [ ] Empty state shows blank or
No results - [ ] Errors render as raw strings or
Error: undefined - [ ] Disabled buttons with no explanation
- [ ] Async actions with no pending state
Navigation smells
- [ ] No active state on current nav item
- [ ] Breadcrumb missing on deep page
- [ ] Page H1 doesn't match the nav label that led here
- [ ] Back button mystery — unclear what it goes back to
What NOT to do
The refactor stays in scope. Hard rules:
- Don't redesign the feature. If the spec says "patient profile form," output a patient profile form. Not a wizard, not a stepper, not a tabs view.
- Don't add abstractions. No new hooks, no
useFormReducer, no extracting "for reuse" unless the original was already duplicated. - Don't rewrite business logic.
handleSubmitstays as-is unless it's the cause of the UX problem. - Don't add dependencies. No new packages. Use what's imported.
- Don't introduce a design system. If none exists, suggest primitives in comments — don't ship a
Button.tsxfile. - Don't add features the user didn't ask for. No extra validation rules, no autosave, no keyboard shortcuts.
- Don't moralize. No paragraphs about why the original was bad. The diff speaks.
Forms checklist
- [ ] Labels above inputs (not inline placeholders standing in for labels)
- [ ] Required fields marked with
*or text — never asterisk-only without legend - [ ] Optional fields marked with
Optional, not parenthetical "(if applicable)" - [ ] Validation runs on blur or submit, not every keystroke
- [ ] Error messages: what's wrong + how to fix, attached to the field
- [ ] User input retained on error (no wiping the form)
- [ ] Submit button shows pending state during async
- [ ] Cancel/back path always visible
- [ ] Primary action visually distinct from secondary
- [ ] Logical field grouping (personal info, contact, payment) with clear separation
- [ ] Tab order matches visual order
State design checklist
Every async/conditional component must handle:
- [ ] Initial / idle — clear what the user can do
- [ ] Loading — skeleton matching final layout, or labeled spinner ("Loading patients…")
- [ ] Empty — explain why empty + offer next action
- [ ] Error — what failed + retry or next step
- [ ] Success — confirmation that doesn't block further action
- [ ] Disabled — explain why (tooltip, inline hint), don't leave it dead
- [ ] Partial / pending — optimistic UI or clear in-progress indicator
Navigation / orientation checklist
- [ ] Where am I? — active state on current item, page title visible
- [ ] What can I do here? — primary action visible above the fold
- [ ] Where can I go? — main nav persistent, related links surfaced
- [ ] Breadcrumb on pages ≥2 levels deep
- [ ] Logo / app name links home
- [ ] Page H1 matches the link or nav label that led here
Mobile / touch checklist
- [ ] Tap targets ≥ 44×44px
- [ ] No hover-only behavior — every hover affordance is also tappable / focusable
- [ ] Forms don't trap zoom (16px+ font on inputs)
- [ ] Modals dismiss with explicit close, not just outside-tap
- [ ] No horizontal scroll on screens ≤ 375px
- [ ] Critical actions never hidden behind hover or right-click
Accessibility baseline
Not optional. These are usability bugs, not extra credit.
- [ ] Semantic HTML:
<button>for actions,<a href>for navigation, headings in order - [ ] All inputs have associated
<label>(viahtmlForor wrapping) - [ ] Focus visible on all interactive elements
- [ ] Color not the only signal (errors include text + icon, not just red)
- [ ] Sufficient contrast (WCAG AA: 4.5:1 body text)
- [ ] Icon-only buttons have
aria-label - [ ] Modals trap focus and restore on close
- [ ] Form errors announced via
aria-liveorrole="alert" - [ ] No
onClickon<div>— use<button>
Copy rules
- Sentence case for labels, headings, buttons. Title Case Looks Shouty.
- Drop "please" — polite but adds reading load.
- Drop "you" / "your" in field labels —
EmailnotYour email. - Use contractions —
Couldn'tnotCould not. - Use the verb of what happens —
Save,Delete patient,Send invoice— notSubmit/OK. - Specific over vague —
Couldn't save changes — try againnotAn error occurred. - Action-oriented empty states —
No patients yet. Add your first patient.notNo data found. - Don't shorten into ambiguity.
Phone (optional)is fine;Phis not.
See references/copy-rewrite-patterns.md for the full lookup table.
Common patterns cheat sheet
| Anti-pattern | Fix |
|---|---|
Inline <Spinner /> only | Use the project's loading primitive with context label |
Empty state shows blank or No results | Explain why + offer next action |
| Error renders raw string | Wrap in error component with retry / next-step CTA |
| Modal dismiss is X-only | Add explicit Cancel + primary CTA at the bottom |
Button labeled Submit / OK / Confirm | Verb of what happens: Save, Delete patient, Send invoice |
| Long instruction paragraph above a form | Delete. Form should self-explain via labels and placeholders |
| Multiple H2s competing on a page | One page-level H1, related sections grouped under one H2 |
| Tab labels duplicate page heading | Trim. "Patient details > Information" → tab says Information only |
| Destructive action same color as safe | Destructive = red/destructive variant + confirmation modal |
| Disabled button with no explanation | Add inline hint or tooltip explaining why |
See references/ux-audit-checklist.md for the full operational checklist, references/component-smell-catalog.md for the named-smell catalog with severity, and examples/ for full before/after refactors (form, empty state, destructive modal, search/filter, checkout, navigation).
Output format
Always end with this exact structure:
[Refactored code or targeted sections]
**UX Improvements:**
- [Specific change] → [Why it reduces cognitive load / which Krug principle]
- ...Rules for the bullet list:
- 3–7 bullets, no more.
- Specific change, tied to the code (not "improved hierarchy" — say "promoted Save button to primary, demoted Cancel to ghost").
- Always include the why — name the Krug principle or the cognitive-load reason.
- No generic praise. No "this is now cleaner."
Component size rules
| Size | Approach |
|---|---|
| < 150 lines | Full refactored component |
| 150–400 lines | Refactor highest-friction sections; flag rest with // UX: … inline comments |
| > 400 lines | Identify top 3 friction points, refactor those, ask which section to prioritize next |
What good looks like
- One obvious primary action per section. No visual competition.
- Labels a first-time user understands without training.
- Zero instruction paragraphs.
- Every state (loading, empty, error, disabled) explicit.
- A new user could navigate it correctly on day one.
- The refactor reads faster than the original. If it's longer, something went wrong.
.DS_Store
node_modules/
*.log
Example: Checkout / payment step refactor
Checkout is the highest-stakes flow in any product — drop-off here is direct revenue loss. The most common failures: vague CTAs, hidden total, generic errors, and walls of instructional copy that erode trust at the moment the user is about to spend money.
Before
export function CheckoutPaymentStep({ cart, onPay }: Props) {
const [card, setCard] = useState({ number: '', expiry: '', cvc: '' });
const [billingSame, setBillingSame] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const submit = async () => {
setLoading(true);
try {
await onPay({ card, billingSame });
} catch (e) {
setError('An error occurred');
} finally {
setLoading(false);
}
};
return (
<div className="p-6">
<h2>Payment</h2>
<p>
Please enter your payment information below. Make sure your card details
are correct before clicking the continue button. We accept all major
credit cards. Your payment will be processed securely.
</p>
{error && <div className="text-red-500">{error}</div>}
<div>
<label>Card number</label>
<input value={card.number} onChange={(e) => setCard({ ...card, number: e.target.value })} />
<label>Expiry</label>
<input value={card.expiry} onChange={(e) => setCard({ ...card, expiry: e.target.value })} />
<label>CVC</label>
<input value={card.cvc} onChange={(e) => setCard({ ...card, cvc: e.target.value })} />
</div>
<div>
<input type="checkbox" checked={billingSame} onChange={(e) => setBillingSame(e.target.checked)} />
<label>Billing address same as shipping</label>
</div>
<div>
<p>Subtotal: €{cart.subtotal}</p>
<p>Shipping: €{cart.shipping}</p>
<p>Tax: €{cart.tax}</p>
<p>Total: €{cart.total}</p>
</div>
<button disabled={loading} onClick={submit}>
{loading ? 'Loading...' : 'Continue'}
</button>
</div>
);
}Problems:
Continuebutton hides the consequence — user doesn't know they're about to be charged.- Total is buried at the bottom in equal-weight text.
- Instructional paragraph ("Please enter your payment information...") adds reading load at the moment trust matters most.
- Generic
An error occurred— gives the user no path forward. Loading...button label gives no context.- No order-summary structure — payment, billing, total all flow into one column.
- No security/trust hint — checkouts that don't visually signal "secure" lose conversions.
- Card number input has no formatting hint, no autocomplete attributes.
- Disabled button on missing fields would be silent — user can't tell why.
After
export function CheckoutPaymentStep({ cart, onPay }: Props) {
const [card, setCard] = useState({ number: '', expiry: '', cvc: '' });
const [billingSame, setBillingSame] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<{ title: string; description: string } | null>(null);
const isValid = card.number.length >= 12 && card.expiry.length === 5 && card.cvc.length >= 3;
const submit = async () => {
setLoading(true);
setError(null);
try {
await onPay({ card, billingSame });
} catch (e) {
setError({
title: 'Payment failed',
description: e.code === 'card_declined'
? 'Your card was declined. Try a different card or contact your bank.'
: 'We couldn\'t complete the payment. Try again, or contact support if it persists.',
});
} finally {
setLoading(false);
}
};
return (
<div className="grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-8">
{/* LEFT: payment form */}
<section aria-labelledby="payment-title">
<Typography id="payment-title" variant="h2">Payment</Typography>
<Typography variant="muted" className="mt-1 flex items-center gap-1">
<LockIcon size={14} aria-hidden /> Secured by Stripe · 256-bit encryption
</Typography>
{error && (
<FormErrorBanner title={error.title} className="mt-4">
{error.description}
</FormErrorBanner>
)}
<form onSubmit={(e) => { e.preventDefault(); submit(); }} className="mt-6 space-y-4">
<Field
label="Card number"
inputMode="numeric"
autoComplete="cc-number"
placeholder="1234 1234 1234 1234"
value={card.number}
onChange={(v) => setCard({ ...card, number: v })}
required
/>
<div className="grid grid-cols-2 gap-3">
<Field
label="Expiry"
autoComplete="cc-exp"
placeholder="MM/YY"
value={card.expiry}
onChange={(v) => setCard({ ...card, expiry: v })}
required
/>
<Field
label="CVC"
inputMode="numeric"
autoComplete="cc-csc"
placeholder="3 digits"
value={card.cvc}
onChange={(v) => setCard({ ...card, cvc: v })}
required
/>
</div>
<Checkbox
checked={billingSame}
onChange={setBillingSame}
label="Use shipping address for billing"
/>
<ButtonLoading
type="submit"
loading={loading}
disabled={!isValid}
disabledHint={!isValid ? 'Fill all card fields to continue' : undefined}
className="w-full"
>
Pay €{cart.total.toFixed(2)}
</ButtonLoading>
</form>
</section>
{/* RIGHT: order summary */}
<aside aria-labelledby="summary-title" className="lg:sticky lg:top-6 h-fit">
<Card>
<CardHeader>
<Typography id="summary-title" variant="h3">Order summary</Typography>
</CardHeader>
<CardContent className="space-y-2">
<SummaryRow label="Subtotal" value={cart.subtotal} />
<SummaryRow label="Shipping" value={cart.shipping} />
<SummaryRow label="Tax" value={cart.tax} />
<Divider />
<SummaryRow label="Total" value={cart.total} emphasis />
</CardContent>
</Card>
</aside>
</div>
);
}What improved
- Button states the verb + amount:
Continue→Pay €24.99. The user sees exactly what happens and how much (Krug: pages should be self-evident; clickable looks clickable). - Total surfaced in a sticky summary card, not buried in a flat list. The most important number is now the most prominent (Krug: visual hierarchy).
- Deleted instructional paragraph about entering payment info — the form is self-evident. Replaced with a tiny security hint where trust signal matters (Krug: omit needless words; reservoir of goodwill).
- Specific error message with title + actionable description (
card_declinedvs generic) → user has a path forward (Krug: no dead ends). - Disabled button now explains why via
disabledHint(Fill all card fields to continue) → no silent disabled state. - Added `autoComplete` attributes (
cc-number,cc-exp,cc-csc) → browser autofill + password manager support → measurable conversion improvement on real checkouts. - Added `inputMode="numeric"` → mobile keyboard shows numeric pad immediately → tap-target / mobile UX.
- Trust signal ("Secured by Stripe · 256-bit encryption") placed where it's read, not buried in footer → conversions on payment forms are sensitive to perceived security.
- Two-column layout separates payment form from order summary → clear grouping (Krug: group related, separate unrelated).
- Semantic landmarks (
aria-labelledby,<section>,<aside>) → screen-reader navigation matches visual structure.
Example: Destructive confirmation modal refactor
Destructive actions are one of the highest-stakes UX surfaces. A vague "Are you sure?" with Yes / No buttons fails on three Krug principles at once: it doesn't state the consequence, doesn't disambiguate the destructive button, and doesn't help the user decide.
Before
export function DeletePatientModal({ open, onClose, onConfirm, patient }: Props) {
return (
<Modal open={open} onClose={onClose}>
<h2>Are you sure?</h2>
<p>This action cannot be undone.</p>
<div>
<button onClick={onClose}>No</button>
<button onClick={onConfirm}>Yes</button>
</div>
</Modal>
);
}Problems:
- Title doesn't say what gets deleted.
- Body is generic — doesn't say what's actually destroyed (appointments? notes? billing?).
- Buttons
Yes/Noforce the user to map back to the question. - Both buttons styled identically — destructive action isn't visually distinct.
- No keyboard or aria-handling clues.
After
export function DeletePatientModal({ open, onClose, onConfirm, patient, loading }: Props) {
const dependentCount = patient.appointmentCount + patient.noteCount;
return (
<Modal open={open} onClose={onClose} aria-labelledby="delete-patient-title">
<Modal.Header>
<Typography id="delete-patient-title" variant="h3">
Delete {patient.firstName} {patient.lastName}?
</Typography>
</Modal.Header>
<Modal.Body>
<Typography variant="body">
This permanently deletes their profile, {patient.appointmentCount} appointments,
and {patient.noteCount} clinical notes.
</Typography>
<Typography variant="muted" className="mt-2">
You can't undo this.
</Typography>
</Modal.Body>
<Modal.Footer>
<Button variant="ghost" onClick={onClose}>Cancel</Button>
<ButtonLoading
variant="destructive"
loading={loading}
onClick={onConfirm}
>
Delete patient
</ButtonLoading>
</Modal.Footer>
</Modal>
);
}UX Improvements:
- Title states exactly what gets deleted, including the patient name → user can decide without re-reading the page (Krug: pages should be self-evident).
- Body lists concrete consequences (appointment count, note count) → user understands the blast radius before confirming, not after.
- Renamed buttons
Yes/No→Cancel/Delete patient→ buttons describe the verb of what happens (Krug: clickable looks clickable, labels match outcome). - Styled destructive button with
variant="destructive"→ visual weight matches consequence (Krug: visual hierarchy beats decoration). - Promoted Cancel as ghost, destructive as primary-style red → user can't accidentally tap the wrong one with similar weights.
- Added pending state via
ButtonLoading→ user gets feedback during the async deletion instead of double-clicking. - Added
aria-labelledby→ modal has a programmatic title for screen readers.
Example: Empty state refactor
Empty states are one of the easiest places to fail Krug's "no dead ends" rule. A blank pane or a bare "No results" tells the user nothing about why it's empty or what to do next.
Before
export function AppointmentsList({ appointments, isLoading, error }: Props) {
if (isLoading) return <Spinner />;
if (error) return <p>Error: {error.message}</p>;
if (appointments.length === 0) {
return <p>No appointments.</p>;
}
return (
<ul>
{appointments.map((a) => <AppointmentRow key={a.id} appointment={a} />)}
</ul>
);
}After
export function AppointmentsList({ appointments, isLoading, error, refetch }: Props) {
if (isLoading) {
return <PageState variant="loading" title="Loading appointments…" />;
}
if (error) {
return (
<PageState
variant="error"
title="Couldn't load appointments"
description="Check your connection and try again."
action={<Button onClick={refetch}>Retry</Button>}
/>
);
}
if (appointments.length === 0) {
return (
<PageState
variant="empty"
title="No appointments yet"
description="Schedule your first appointment to start tracking visits."
action={<Button onClick={openNewAppointment}>New appointment</Button>}
/>
);
}
return (
<ul className="divide-y">
{appointments.map((a) => <AppointmentRow key={a.id} appointment={a} />)}
</ul>
);
}UX Improvements:
- Replaced bare
No appointments.withPageState→ empty state now explains why and offers a clear next action (Krug: no dead ends). - Replaced raw
Error: {error.message}with structured error state including a Retry CTA → user has a path forward, not a wall. - Replaced silent
<Spinner />with labeled loading state → user knows what is loading, not just that something is. - Used DS
PageStateconsistently across all three states → predictable layout, accessible by default, easier to scan. - Surfaced primary CTAs (
New appointment,Retry) inline with the state → user doesn't have to hunt the rest of the page.
Example: Patient profile form refactor
Typical settings/profile form. Shows the most common ux-enhancer transformations: deleting happy talk, tightening labels, surfacing the primary CTA, using DS state primitives, and proper error/loading handling.
Before
export function PatientProfileForm({ patient }: { patient: Patient }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-2">Welcome to your profile!</h1>
<p className="text-gray-600 mb-6">
On this page you can view and update your personal information.
Please make sure all the fields are filled out correctly before
clicking the submit button at the bottom of the form.
</p>
{error && <div className="text-red-500">{error}</div>}
<form onSubmit={handleSubmit}>
<label>Please enter your first name:</label>
<input name="firstName" defaultValue={patient.firstName} />
<label>Please enter your last name:</label>
<input name="lastName" defaultValue={patient.lastName} />
<label>What is your email address?</label>
<input name="email" type="email" defaultValue={patient.email} />
<label>Phone number (optional, but recommended):</label>
<input name="phone" defaultValue={patient.phone} />
{loading ? <Spinner /> : null}
<button type="button">Cancel</button>
<button type="submit">Submit changes</button>
</form>
</div>
);
}After
export function PatientProfileForm({ patient }: { patient: Patient }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
return (
<Card>
<CardHeader>
<Typography variant="h2">Profile</Typography>
</CardHeader>
<CardContent>
{error && <FormErrorBanner>{error}</FormErrorBanner>}
<form onSubmit={handleSubmit} className="space-y-4">
<Field label="First name" name="firstName" defaultValue={patient.firstName} required />
<Field label="Last name" name="lastName" defaultValue={patient.lastName} required />
<Field label="Email" name="email" type="email" defaultValue={patient.email} required />
<Field label="Phone" name="phone" hint="Optional" defaultValue={patient.phone} />
<div className="flex justify-end gap-2 pt-2">
<Button variant="ghost" type="button">Cancel</Button>
<ButtonLoading loading={loading} type="submit">Save</ButtonLoading>
</div>
</form>
</CardContent>
</Card>
);
}UX Improvements:
- Deleted happy-talk paragraph and "Welcome to your profile!" heading → users scan, they don't read intros (Krug: omit needless words).
- Compressed labels (
Please enter your first name→First name) → reduces reading load while staying specific. - Marked required fields explicitly with
requiredprop instead of leaving validation implicit → surfaces constraints up front (Krug: forms forgiving and obvious). - Replaced inline
<Spinner />withButtonLoading→ loading state lives where the action is, not floating in the form. - Replaced raw error
<div>withFormErrorBanner→ consistent error UX, semantic for screen readers. - Promoted
Save(primary) overCancel(ghost) → primary action visually distinct. - Renamed
Submit changes→Save→ button states the verb of what happens, not the form mechanic. - Wrapped in
Card+Typography→ DS hierarchy instead of ad-hoctext-2xl font-bold.
Example: Dashboard sidebar / navigation refactor
Dashboard sidebars are where Krug's "Where am I? What can I do? Where can I go?" gets tested most aggressively. Common failures: 15 flat items at equal weight, no active state, vague labels, and no obvious place for account/settings/logout.
Before
export function DashboardSidebar({ user, currentPath }: Props) {
return (
<nav className="w-60 bg-gray-100 p-4">
<div className="text-xl mb-6">MyApp</div>
<ul>
<li><a href="/dashboard"><HomeIcon /></a></li>
<li><a href="/manage-patients">Manage Patients</a></li>
<li><a href="/appointments-calendar">Appointments Calendar</a></li>
<li><a href="/billing-and-invoices">Billing and Invoices</a></li>
<li><a href="/reports-section">Reports Section</a></li>
<li><a href="/team-members">Team Members</a></li>
<li><a href="/integrations">Integrations</a></li>
<li><a href="/settings-page">Settings Page</a></li>
<li><a href="/profile">Profile</a></li>
<li><a href="/notifications">Notifications</a></li>
<li><a href="/help-center">Help Center</a></li>
<li><a href="/logout">Log out</a></li>
</ul>
</nav>
);
}Problems:
- 12 items, flat list, equal weight — user can't tell what's primary.
- No active state — user can't tell which page they're on.
- Vague / redundant labels (
Manage Patientsshould bePatients;Reports Sectionshould beReports). - First item is icon-only with no label and no
aria-label→ mystery meat. - No grouping — work items, account, and help-link all flow together.
- Logout sits alphabetically among work pages — destructive action treated as ordinary nav.
- No mobile pattern — fixed 240px sidebar will break on small screens.
- No way to know where account/settings live without reading every item.
After
type NavItem = { label: string; href: string; icon: ReactNode };
const PRIMARY_NAV: NavItem[] = [
{ label: 'Dashboard', href: '/dashboard', icon: <HomeIcon /> },
{ label: 'Patients', href: '/patients', icon: <UsersIcon /> },
{ label: 'Appointments', href: '/appointments', icon: <CalendarIcon /> },
{ label: 'Billing', href: '/billing', icon: <CreditCardIcon /> },
{ label: 'Reports', href: '/reports', icon: <ChartIcon /> },
];
const SECONDARY_NAV: NavItem[] = [
{ label: 'Team', href: '/team', icon: <TeamIcon /> },
{ label: 'Integrations', href: '/integrations', icon: <PlugIcon /> },
];
export function DashboardSidebar({ user, currentPath }: Props) {
const [mobileOpen, setMobileOpen] = useState(false);
return (
<>
{/* Mobile trigger */}
<button
className="lg:hidden p-3"
aria-label="Open navigation"
aria-expanded={mobileOpen}
onClick={() => setMobileOpen(true)}
>
<MenuIcon />
</button>
<nav
aria-label="Main"
className={cn(
'flex flex-col w-60 h-screen bg-surface border-r',
'lg:static lg:translate-x-0',
mobileOpen ? 'fixed inset-y-0 left-0 z-50' : 'hidden lg:flex',
)}
>
{/* Brand */}
<div className="px-4 py-5 border-b">
<Logo />
</div>
{/* Work — primary */}
<ul className="flex-1 overflow-y-auto p-2 space-y-1">
{PRIMARY_NAV.map((item) => (
<NavLink key={item.href} item={item} active={currentPath.startsWith(item.href)} />
))}
{/* Secondary group, visually demoted */}
<li className="pt-4">
<div className="px-3 pb-1 text-xs uppercase tracking-wide text-muted">
Workspace
</div>
<ul className="space-y-1">
{SECONDARY_NAV.map((item) => (
<NavLink key={item.href} item={item} active={currentPath.startsWith(item.href)} />
))}
</ul>
</li>
</ul>
{/* Account — anchored bottom, predictable placement */}
<div className="border-t p-3">
<DropdownMenu>
<DropdownMenu.Trigger className="flex items-center gap-2 w-full p-2 rounded hover:bg-muted">
<Avatar src={user.avatarUrl} name={user.name} size={28} />
<div className="flex-1 text-left">
<div className="text-sm font-medium">{user.name}</div>
<div className="text-xs text-muted">{user.email}</div>
</div>
<ChevronUpIcon size={14} />
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item href="/profile">Profile</DropdownMenu.Item>
<DropdownMenu.Item href="/settings">Settings</DropdownMenu.Item>
<DropdownMenu.Item href="/help">Help center</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item href="/logout" destructive>Log out</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
</div>
</nav>
</>
);
}
function NavLink({ item, active }: { item: NavItem; active: boolean }) {
return (
<li>
<a
href={item.href}
aria-current={active ? 'page' : undefined}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded text-sm',
active
? 'bg-primary/10 text-primary font-medium'
: 'text-fg hover:bg-muted',
)}
>
<span aria-hidden>{item.icon}</span>
{item.label}
</a>
</li>
);
}What improved
- Grouped nav into work + workspace + account — three clear zones instead of 12 flat items (Krug: group related, separate unrelated).
- Obvious active state with background tint, primary color, font weight, and
aria-current="page"→ user can answer "where am I?" instantly (Krug: navigation must answer Where am I?). - Tightened labels (
Manage Patients→Patients,Appointments Calendar→Appointments,Reports Section→Reports) → removes redundancy, matches page H1 (Krug: page titles match clicked links). - All icons paired with text labels — no mystery-meat icon-only items. Decorative icons marked
aria-hiddenso screen readers don't read them twice. - Account / settings / help / logout moved to a single bottom-anchored menu with a destructive variant on Log out → predictable placement and destructive action is visually distinct (Krug: convention; clickable looks clickable).
- Mobile pattern: nav collapses behind a labeled
Open navigationbutton, slides in as overlay on small screens.aria-expandedreflects state. - Semantic landmarks:
<nav aria-label="Main">→ screen reader can jump to nav directly. - Pinned secondary group label ("Workspace" subheading) → visual hierarchy signals that those items are less frequent than primary work.
Mobile-friendly notes
- The same component renders as off-canvas overlay on
< lg(1024px) screens. - The trigger is visible and labeled, not a bare hamburger glyph.
- Tap targets on nav links are ≥44px tall via
py-2 px-3. - The bottom user dropdown is reachable on mobile because the entire sidebar is scrollable above it.
Example: Search + filter toolbar refactor
Search/filter UIs fail when they don't show what's currently applied, don't hint at what's searchable, and don't tell the user how many results matched. Users end up confused about why they're seeing what they're seeing.
Before
export function PatientsToolbar({ filters, onChange, onSearch }: Props) {
return (
<div className="flex gap-2">
<input
type="text"
placeholder="Search"
onChange={(e) => onSearch(e.target.value)}
/>
<select onChange={(e) => onChange({ ...filters, status: e.target.value })}>
<option value="">Filter</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<select onChange={(e) => onChange({ ...filters, range: e.target.value })}>
<option value="">Filter</option>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
</select>
<button onClick={() => onChange({})}>Clear</button>
</div>
);
}Problems:
Filterdropdown placeholder is meaningless — what does it filter?Searchplaceholder gives no hint at what's searchable.- No active-filter indicator — user can't see what's applied without inspecting selects.
Clearis ambiguous — clear search? filters? both?- No result count, no "showing N of M" feedback.
After
export function PatientsToolbar({
filters,
onChange,
onSearch,
total,
filteredCount,
}: Props) {
const activeFilters = getActiveFilters(filters); // [{ key: 'status', label: 'Active' }, …]
const hasFilters = activeFilters.length > 0;
return (
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
<SearchInput
placeholder="Search by name, email, or ID"
onChange={onSearch}
className="flex-1 min-w-[240px]"
/>
<Select
label="Status"
value={filters.status ?? ''}
onChange={(v) => onChange({ ...filters, status: v })}
options={[
{ value: '', label: 'All statuses' },
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
]}
/>
<Select
label="Date range"
value={filters.range ?? ''}
onChange={(v) => onChange({ ...filters, range: v })}
options={[
{ value: '', label: 'All time' },
{ value: '7d', label: 'Last 7 days' },
{ value: '30d', label: 'Last 30 days' },
]}
/>
</div>
<div className="flex items-center justify-between">
<Typography variant="muted">
{hasFilters
? `Showing ${filteredCount} of ${total} patients`
: `${total} patients`}
</Typography>
{hasFilters && (
<div className="flex flex-wrap items-center gap-2">
{activeFilters.map((f) => (
<Chip
key={f.key}
onRemove={() => onChange({ ...filters, [f.key]: undefined })}
>
{f.label}
</Chip>
))}
<Button variant="ghost" size="sm" onClick={() => onChange({})}>
Clear filters
</Button>
</div>
)}
</div>
</div>
);
}UX Improvements:
- Search placeholder now says what's searchable (
name, email, or ID) → user doesn't have to guess (Krug: self-evident). - Renamed dropdown placeholders from generic
Filterto the actual filter dimension (Status,Date range) → the control labels itself. - Added active-filter chips with individual remove → user sees exactly what's applied and can drop one without resetting all (Krug: where am I?).
- Added result count (
Showing 23 of 412) → user understands why the list looks the way it does. - Renamed
Clear→Clear filtersand only shows when filters are active → button label states the verb, no dead control when nothing to clear. - Added explicit "All statuses" / "All time" options → empty filter is now a real choice, not a mystery placeholder.
MIT License
Copyright (c) 2026 Artim Gashi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
ux-enhancer
A Claude Code skill that refactors React components for usability.
It applies Steve Krug's Don't Make Me Think principles — visual hierarchy, scanning-optimized layout, ruthless copy reduction, unambiguous CTAs, and explicit loading/empty/error states — to existing components without redesigning the feature, adding abstractions, or rewriting business logic.
What it does
You hand it a React component. It:
- Cuts happy talk, instruction paragraphs, verbose labels.
- Surfaces the primary action so it's actually obvious.
- Replaces silent loading, dead-end empty states, and raw error strings with proper state UI.
- Tightens microcopy (
Please enter your first name→First name,Submit→Save). - Fixes hierarchy and grouping so the screen scans in 3 seconds.
- Uses your existing design system instead of inventing new components.
When to use
Use it when you want to:
- Reduce cognitive load on a settings page, form, modal, or dashboard.
- Apply Krug's Don't Make Me Think rules in one pass.
- Audit a component for usability smells (run the checklist).
- Improve labels, CTAs, hierarchy, states, forms, or navigation.
- Make a task-driven workflow more intuitive.
When not to use
- Backend / API / DB / business logic — no UI to refactor.
- Pure performance work — use
vercel-react-best-practices. - Visual branding, animation, or building a landing page from scratch — use
frontend-designorimpeccable. - Fresh designs from a brief — this skill refactors existing UI.
Install
Via the skills.sh CLI:
npx skills add gashiartim/ux-enhancerOr clone manually:
git clone https://github.com/gashiartim/ux-enhancer.git ~/.claude/skills/ux-enhancerHow to invoke
Once installed, the skill auto-triggers on UX-related requests. You can also invoke it explicitly:
/ux-enhancerOr in a normal prompt:
Apply ux-enhancer to this component.
Refactor this for usability.
Run a UX audit on this file.
What you get back
The skill always returns:
1. Refactored code (full component or targeted sections, depending on size). 2. A 3–7 bullet `UX Improvements:` list mapping each significant change to the cognitive-load reason or Krug principle it addresses.
Example output structure:
// refactored component …UX Improvements:
- Deleted happy-talk paragraph → users scan, they don't read intros (Krug: omit needless words).
- CompressedPlease enter your first name→First name→ reduces reading load.
- PromotedSaveto primary, demotedCancelto ghost → primary action now visually distinct.
Core principles
1. Don't make users think. 2. Pages should be self-evident or self-explanatory. 3. Users scan, they don't read. 4. Users satisfice — they pick the first plausible option. 5. Conventions over cleverness. 6. Visual hierarchy beats decorative UI. 7. Cut needless words ruthlessly. 8. Kill happy talk. 9. Empty / loading / error states must guide the next action. 10. Mobile UX and accessibility are part of usability, not optional layers.
Example before / after
Before:
<h1>Welcome to your profile!</h1>
<p>On this page you can update your personal information. Please make
sure all fields are filled out correctly before clicking submit.</p>
<label>Please enter your first name:</label>
<input name="firstName" />
<button>Submit changes</button>After:
<Typography variant="h2">Profile</Typography>
<Field label="First name" name="firstName" required />
<Button type="submit">Save</Button>See `examples/` for full before/after refactors of forms, empty states, destructive modals, search/filter toolbars, checkout flows, and dashboard navigation.
Compatibility
The skill auto-detects the project's design system and uses its primitives. Tested patterns for:
- shadcn/ui
- Material UI (MUI)
- Chakra UI
- Mantine
- Ant Design
- Custom in-house design systems
If no DS exists, the skill suggests primitives in inline comments (// New pattern — DS gap) instead of inventing components.
What's inside
- `SKILL.md` — main instructions, workflow, checklists, output format
- `references/copy-rewrite-patterns.md` — verbose copy → tightened lookup table
- `references/ux-audit-checklist.md` — operational checklist for auditing components
- `references/component-smell-catalog.md` — 15 named UX smells with severity and refactor rules
- `examples/` — before/after React refactors: form, empty state, destructive modal, search/filter, checkout, navigation
Contributing
Issues and PRs welcome. Especially valuable: real before/after examples from your own codebase that show a class of problem the skill doesn't yet handle.
License
MIT
Component smell catalog
Practical UX smells with symptoms, severity, and a refactor rule. Use this as a scan-before-refactor inventory: walk the catalog, mark hits in the component, prioritize by severity, then fix.
Severity legend:
- Blocker — user can't complete the primary task
- High — friction strong enough to cause abandonment
- Medium — slows the user but doesn't block
- Low — polish
---
1. Vague button smell
Symptom: Button labeled Submit, OK, Confirm, Continue, Click here, Done, or Save changes. Why it hurts: User has to map the button back to context to know what it does. Each map costs a beat of cognition. On checkout / destructive actions, this beat is where uncertainty grows. Bad: <Button>Submit</Button> Better: <Button>Pay €24.99</Button> / <Button>Save</Button> / <Button variant="destructive">Delete patient</Button> Severity: High (Blocker on checkout / destructive flows). Refactor rule: Button label = the verb of what happens, not the form mechanic.
---
2. Instruction paragraph smell
Symptom: A <p> of explanatory text above a form, list, or modal explaining how to use the UI. Why it hurts: If the UI needs prose to explain itself, the design is the bug. Users scan, they don't read instructions. Bad:
<p>Please enter your payment information below. Make sure your card details
are correct before clicking continue.</p>Better: Delete entirely. Tighten field labels and button copy so the form is self-evident. Severity: Medium (High on first-use / onboarding). Refactor rule: Delete instruction paragraphs. Fix the design so they aren't needed.
---
3. Dead-end empty state smell
Symptom: Empty UI shows nothing, "No results", "0 items", or "No data found" with no path forward. Why it hurts: User doesn't know whether the empty state is broken, filtered, or genuinely empty — and has nowhere to go. Bad: {items.length === 0 && <p>No results</p>} Better:
<PageState
variant="empty"
title="No patients yet"
description="Add your first patient to start tracking visits."
action={<Button onClick={openNew}>Add patient</Button>}
/>Severity: High (Blocker for first-time / new-account users). Refactor rule: Empty state must explain why it's empty AND offer a next action.
---
4. Generic error smell
Symptom: Error renders as Error, An error occurred, Something went wrong, Failed, or a raw status code. Why it hurts: User can't tell whether to retry, fix something, or contact support. Worst when the cause is fixable (declined card, expired session) but the message is generic. Bad: <div>An error occurred</div> Better: <FormErrorBanner title="Payment failed">Your card was declined. Try a different card or contact your bank.</FormErrorBanner> Severity: High. Refactor rule: Error must say (a) what failed, (b) what the user can do next. Distinguish recoverable from unrecoverable.
---
5. Hidden primary action smell
Symptom: The most-common action is visually equal to or quieter than secondary actions. Often: ghost-styled "Save" next to a brightly-colored "Cancel," or the primary CTA below the fold. Why it hurts: Users satisfice — they pick the first plausible-looking action. If primary isn't the most prominent, they pick the wrong one or hesitate. Bad: Three buttons in a row, all the same color, primary in the middle. Better: One primary (filled), one secondary (ghost / outline), one destructive (red) where applicable. Primary positioned right (or wherever the project's convention places it). Severity: High. Refactor rule: Exactly one primary per section. Visually distinct by color, weight, and position.
---
6. Mystery meat navigation smell
Symptom: Icons without labels, icon-only buttons without aria-label, ambiguous glyphs the user has to decode. Why it hurts: User has to hover or click to learn what something does. On mobile, hover doesn't exist. Bad: <button><HomeIcon /></button> Better: Pair the icon with a visible label, or at minimum: <button aria-label="Home"><HomeIcon /></button> with a tooltip on hover/focus. Severity: Medium (High on mobile / accessibility-sensitive contexts). Refactor rule: No icon-only controls without aria-label and a tooltip. Prefer icon + label whenever space allows.
---
7. Backend-language leak smell
Symptom: UI surfaces engineering terms: User ID, Authenticate, Endpoint, 200 OK, null, undefined, raw enum values like INVOICE_STATUS_PENDING_REVIEW. Why it hurts: User sees the system internals and has to translate. Worse: feels like a leaked debug screen, erodes trust. Bad: <Badge>INVOICE_STATUS_PENDING_REVIEW</Badge> Better: <Badge>Pending review</Badge> Severity: Medium. Refactor rule: Translate every backend constant into human language at the UI boundary. Sentence case, no underscores, no codes.
---
8. Hover-only action smell
Symptom: Edit / delete / download buttons that only appear on row hover. Tooltip-only critical info. Menu-on-hover. Why it hurts: Hover doesn't exist on touch devices. Even on desktop, users don't know the action is there until they happen over the right area. Bad: <Row className="group">{name}<Button className="opacity-0 group-hover:opacity-100">Edit</Button></Row> Better: Persistent action button, or a … overflow menu that's always visible. Severity: High on mobile-relevant apps; Medium otherwise. Refactor rule: Every hover affordance must also be tappable / focusable / discoverable without hover.
---
9. Fake disabled state smell
Symptom: Button is disabled (opacity-50, cursor-not-allowed) with no explanation of why. User can't tell what to fix. Why it hurts: Disabled looks identical whether it's "fill more fields" or "you don't have permission" or "the system is broken." Bad: <Button disabled={!isValid}>Save</Button> Better:
<Button
disabled={!isValid}
disabledHint={!isValid ? 'Fill all required fields to save' : undefined}
>
Save
</Button>or skip disabled entirely — let the user click, then show inline validation errors. Severity: High. Refactor rule: Disabled controls must explain why via tooltip, inline hint, or aria-describedby.
---
10. Overloaded modal smell
Symptom: Modal contains a multi-step form, a secondary tab, an inline list, OR has more than one primary action. Why it hurts: Modals are interruptions. If the task needs scrolling, tabs, or multiple distinct decisions, it deserves a page. Bad: A modal with Personal info / Billing / Preferences tabs and a Save button. Better: Move it to a dedicated page or split into a wizard. Keep modals focused on one decision. Severity: Medium. Refactor rule: Modal = one task, one primary action. If you have tabs in a modal, you have a page.
---
11. Equal visual weight smell
Symptom: All headings the same size; all buttons the same color; sidebar items all bold; a wall of cards with identical treatment. Why it hurts: No hierarchy = no scannability. User can't tell what matters in 3 seconds. Bad: 8 dashboard cards, identical size and color, no visual emphasis on the key metric. Better: 1–2 hero cards (larger, primary color accent), secondary cards in a smaller grid below. Severity: Medium (High on dashboards and landing screens). Refactor rule: Important things must look important. Use size, weight, color, position deliberately.
---
12. Unforgiving form input smell
Symptom: Form wipes user input on validation error. Validates every keystroke. Rejects whitespace, dashes, or formatting in phone/card numbers. Doesn't preserve fields on session timeout. Why it hurts: Users feel punished for mistakes. Trust erodes. Abandonment spikes. Bad: <input pattern="\d+" /> rejecting 1234-5678. Better: Accept formatted input, normalize server-side. Validate on blur or submit, not on every keystroke. Preserve user input on error. Severity: High (especially on payment / signup / contact forms). Refactor rule: Forms forgive. Normalize formatting in code, not on the user.
---
13. Missing orientation smell
Symptom: User can't tell where they are in the app. No active nav state, no breadcrumb on deep page, no page title, page H1 doesn't match the link that led there. Why it hurts: User can't answer "Where am I?" — Krug's first navigation question. Disorients especially on multi-step flows. Bad: Sidebar with no active state on the current page. Better: aria-current="page", primary-color tint, breadcrumb on pages ≥2 deep, H1 matches nav label. Severity: Medium (High on multi-step flows). Refactor rule: Every page must answer Where am I / What can I do / Where can I go.
---
14. Confirmation without consequence smell
Symptom: Destructive confirmation modal says only Are you sure? or This cannot be undone without saying what gets destroyed. Why it hurts: User can't make an informed decision. Often the safer option (Cancel) gets clicked out of caution, or the wrong button gets clicked because consequences weren't clear. Bad: Title Are you sure?, body This action cannot be undone., buttons Yes / No. Better: Title Delete Maria Lopez?, body This permanently deletes their profile, 23 appointments, and 8 clinical notes., buttons Cancel / Delete patient (destructive variant). Severity: High. Refactor rule: Confirmation states the specific consequence. Buttons name the specific action.
---
15. Loading state that hides context smell
Symptom: Spinner replaces the entire screen during partial loading. Disabled-button-with-no-feedback during async submit. Skeleton wildly different from the final layout. Why it hurts: User loses context — were they on the right page? Did the click register? Will the data come back? Bad: if (loading) return <Spinner /> over a settings page. Better: Skeleton matching final layout, OR optimistic update, OR per-section loading. Buttons show spinner inside with label change (Saving…). Severity: Medium. Refactor rule: Loading should preserve context. Never replace the whole screen with a centered spinner unless the page truly has nothing useful to show yet.
---
How to use this catalog
1. Before refactoring, walk the catalog. Mark every hit. 2. Sort hits by severity (Blocker → High → Medium → Low). 3. Fix Blockers first, then High. Don't waste a turn on Low while a Blocker exists. 4. In the **UX Improvements:** output, name the smell (e.g. "Vague button smell — Submit → Pay €24.99"). 5. If you find a smell not in the catalog, flag it with a name and a one-line refactor rule. PRs welcome.
Copy rewrite patterns
Lookup table for verbose UI copy and its tightened replacement. Apply by default unless context demands otherwise.
Rule of thumb: shorter is better only when it stays specific and action-oriented. Submit is shorter than Save changes, but worse. Don't shorten into ambiguity.
---
Welcomes / scene-setting → delete
| Before | After |
|---|---|
| "Welcome to your profile! Here you can update your personal information." | (delete) |
| "This is your dashboard where you can see all your data." | (delete) |
| "Use the form below to..." | (delete — the form is self-evident) |
| "Click the button to..." | (delete — buttons explain themselves via label) |
| "On this page you can..." | (delete — the page exists; users can already see what's possible) |
| "Hi [Name], welcome back!" in product UI | (delete or move to a tiny header — never block the work) |
Form labels → tighten, keep specific
| Before | After |
|---|---|
| "Please enter your first name" | Label First name |
| "What is your email address?" | Label Email |
| "Phone number (optional, but recommended)" | Label Phone, hint Optional |
| "Please select your country from the list below" | Label Country |
| "Type a password (must be at least 8 characters)" | Label Password, hint 8+ characters |
| "Your home address" | Label Address |
| "Confirm your email address" | Label Confirm email |
| "Date of birth (MM/DD/YYYY)" | Label Date of birth, hint MM/DD/YYYY |
Don't over-shorten: Phone not Ph, Address not Addr. Trust the user to read words; don't make them decode abbreviations.
Buttons → verb of what happens
| Before | After |
|---|---|
Submit | Save, Send, Create, Pay — whichever applies |
OK | The verb of the action, or Confirm if generic |
Click here | The verb of the action |
Save changes | Save |
Continue (when it submits) | Save and continue or just Save |
Yes, I want to delete this item | Delete |
Add new item to your list | Add item |
Done (in a settings modal) | Save if it persists, Close if it doesn't |
Update your information | Save |
Send invoice to customer | Send invoice |
Keep `Cancel` in modals — it's a standard escape, well-understood.
Empty states → explain + offer next action
| Before | After |
|---|---|
| "No results" | "No patients yet. Add your first patient." + primary CTA |
| "Empty" | "You haven't created any invoices. New invoice →" |
| "No data found" | "No appointments scheduled today. Schedule one →" |
| Blank pane | Always: icon + 1-line explanation + 1 CTA |
| "0 items" | "Your cart is empty. Browse products →" |
| "Nothing here" | "No notifications. We'll let you know when something needs attention." |
Error messages → say what's wrong AND what to do
| Before | After |
|---|---|
| "Error" | "Couldn't save changes. Try again." + Retry button |
| "Invalid input" | "Email must contain @" |
| "Something went wrong" | "We couldn't load your appointments. Refresh, or contact support if it persists." |
| "Failed" | "Payment failed: card declined. Try a different card." |
| "Required" (under a field) | "Email is required" |
| "Network error" | "Lost connection. Check your internet and try again." |
| "Unauthorized" | "Your session expired. Sign in again." |
500: Internal Server Error | "Something broke on our end. We've been notified — try again in a minute." |
Loading states → never silent
| Before | After |
|---|---|
| Blank screen | Skeleton matching final layout |
| Generic spinner | Skeleton, OR spinner + 1-line context: "Loading patients…" |
| Disabled button no feedback | Spinner inside button, label changes to "Saving…" |
| Frozen UI during save | Optimistic update, or pending row indicator |
Confirmation modals → state the consequence
| Before | After |
|---|---|
| Title: "Are you sure?" | Title: "Delete this patient?" |
| Body: "This action cannot be undone." | Body: "This permanently deletes 23 appointments and their notes." |
Buttons: Yes / No | Buttons: Cancel / Delete patient (destructive variant) |
Buttons: OK / Cancel | Buttons: Cancel / Confirm (or the verb) |
| "Confirm action" (generic) | Title with the actual action: "Cancel subscription?" |
Navigation labels → match the destination
| Before | After |
|---|---|
Sidebar: Manage Patients | Patients (matches H1 on the page) |
Sidebar: Patient Management Console | Patients |
Tab: Patient Information Details | Information (page already says "Patient details") |
Link: Click here to view your settings | Link: Settings |
Breadcrumb: Home > Section > Subsection > This page | Breadcrumb: Patients > Maria Lopez > Profile (specific names) |
Settings pages → group + explain
| Before | After |
|---|---|
| Flat list of 30 toggles | Grouped sections with H2: Account, Notifications, Privacy, Billing |
| Toggle label: "Enable notifications" | Toggle label: Email notifications, hint: Get an email when something needs attention |
| Save button at top of long form | Sticky save bar at bottom; show only when settings change |
| "Settings" page with no entry-point hierarchy | Subnav or table-of-contents on left for >5 sections |
Dashboard cards → primary metric + comparison
| Before | After |
|---|---|
Card title: Statistics, body: 1,234 | Card: Active patients, value: 1,234, sub: +12 this week |
Title: Data | Title: the actual metric — Revenue, Open tickets, Conversion |
Last updated: 2026-05-08T14:23:11.123Z | Updated 5 min ago |
| 8 cards, all equal weight | 1–2 hero metrics, smaller secondary cards below |
Checkout / payment flows → state cost + step
| Before | After |
|---|---|
Button: Submit Order | Button: Pay $42.99 |
Step indicator: Step 2 | Step indicator: Step 2 of 3 · Shipping |
Apply (next to coupon field) | Apply code |
Confirmation: Thank you for your purchase! | Confirmation: Order #1234 confirmed. Receipt sent to maria@example.com. |
| Body: paragraph of legal text | Inline expandable: View terms (collapsed by default) |
Search / filter UI → show what's applied
| Before | After |
|---|---|
| Empty filter panel, results unchanged | Show active filter chips above results: Status: Active ×, Date: Last 30 days × |
| Search box with no placeholder | Placeholder: Search by name, email, or ID |
Clear (vague) | Clear filters or Reset |
Filter dropdown labeled Filter | Labeled with what it filters: Status, Date range, Assigned to |
| No result-count feedback | Showing 23 of 412 patients above the list |
Permission / access-denied states → explain + offer path
| Before | After |
|---|---|
403 Forbidden | "You don't have access to this page. Ask an admin to grant the billing.view permission." |
Unauthorized | "Sign in to view this page." + Sign in CTA |
| Blank page on no-access | Explain what's required and who to contact |
Insufficient permissions | "Only clinic admins can edit this. [name@clinic.com] is your admin." |
Destructive actions → consequence + confirmation
| Before | After |
|---|---|
Button: Delete (in flat row, no warning) | Button: Delete (destructive variant — red), opens confirmation modal |
Modal: Are you sure you want to delete? | Modal title: Delete Maria Lopez's profile? + body listing what gets removed |
| Confirmation by single click | Type-to-confirm for irreversible actions: Type DELETE to confirm |
| Auto-archive with no notice | Snackbar with Undo action for 5–10s |
Onboarding / setup steps → state progress + value
| Before | After |
|---|---|
Step 1 | Step 1 of 4 · Add your clinic info |
Welcome! Let's get started. | Set up your clinic in 2 minutes. |
Skip link tiny in corner | Persistent Skip for now if optional, or block + explain why required |
Continue button | Verb of step: Add staff, Connect calendar, Finish setup |
---
Tone rules
- Sentence case for labels and buttons.
Save changes, notSave Changes. - Drop "please" — polite but adds reading load.
- Drop "you" / "your" in field labels —
EmailnotYour email. - Use contractions —
Couldn'tnotCould not. - Avoid marketing voice in product UI — no
amazing,powerful,simply,easily. - Avoid jargon the user didn't pick — say
Sign in, notAuthenticate. - Use numerals for numbers —
5 patients, notfive patients. - Avoid exclamation marks except in legitimate celebrations (
Order confirmed!ok,Welcome!not).
UX audit checklist
A practical, run-down-the-list checklist for auditing a React component. Walk through each section, mark hits, then refactor.
---
First glance test (3-second rule)
A first-time user should be able to answer in 3 seconds:
- [ ] What is this screen?
- [ ] What can I do here?
- [ ] What is the most important action?
- [ ] What can I ignore?
If any answer requires scrolling, reading paragraphs, or hunting — it's a bug.
Primary action test
- [ ] Is there exactly one most-important action per section?
- [ ] Is it the most visually prominent element (size, color, position)?
- [ ] Is the button label the verb of what happens (
Save, notSubmit)? - [ ] Does the action confirm completion clearly (success state, redirect, snackbar)?
- [ ] If destructive, does it require confirmation proportional to impact?
Scanning test
- [ ] Strong visual hierarchy — H1 > H2 > body, distinguishable at a glance
- [ ] Related items grouped, unrelated items separated
- [ ] No walls of equal-weight text
- [ ] Lists where lists make sense, prose where prose makes sense
- [ ] Critical info above the fold (no scrolling for primary task)
Copy test
- [ ] Zero happy talk ("Welcome to your...")
- [ ] Zero instruction paragraphs
- [ ] Field labels ≤ 3 words where possible
- [ ] Buttons use the verb of what happens
- [ ] Errors say what's wrong AND how to fix
- [ ] Empty states explain why empty AND offer a next action
- [ ] No marketing voice ("amazing," "powerful," "simply")
- [ ] No jargon the user didn't bring (
Authenticate→Sign in) - [ ] Sentence case for labels and buttons
- [ ] Contractions used (
Couldn't, notCould not)
Clickability test
- [ ] Buttons look like buttons (not text with hover effects)
- [ ] Links look like links (consistent style across the app)
- [ ] No
cursor: pointeron non-interactive elements - [ ] Focus visible on all interactive elements
- [ ] Hover state never required to discover a control
- [ ] Disabled state explains why (tooltip or inline hint)
Navigation / orientation test
- [ ] Active page indicated in main nav
- [ ] Page H1 matches the link/nav label that led here
- [ ] Breadcrumb on pages ≥2 levels deep
- [ ] Logo / app name links to a sensible home
- [ ] Back button goes where the user expects (not router.back() blindly)
- [ ] User can answer: Where am I? What can I do? Where can I go?
Form test
- [ ] Labels above inputs, not placeholder-only
- [ ] Required fields marked (asterisk + legend, or
Requiredtext) - [ ] Optional fields marked
Optionalif not obvious - [ ] Validation runs on blur or submit, not every keystroke
- [ ] Error messages attached to the offending field
- [ ] User input retained on error
- [ ] Tab order matches visual order
- [ ] Submit button shows pending state during async
- [ ] Cancel/back path always available
- [ ] Logical grouping with visible separation
- [ ] Long forms broken into sections with headings
State test
Every async / conditional component must handle:
- [ ] Idle — clear what the user can do
- [ ] Loading — skeleton or labeled spinner, never silent
- [ ] Empty — explain why + next action, never blank
- [ ] Error — what failed + retry/next step, never raw string
- [ ] Success — confirmation that doesn't block further action
- [ ] Disabled — explain why
- [ ] Pending / partial — optimistic update or in-progress indicator
Mobile / touch test
- [ ] Tap targets ≥ 44×44px
- [ ] No hover-only behavior — every hover affordance also tappable
- [ ] Inputs use 16px+ font (prevents iOS zoom)
- [ ] No horizontal scroll at ≤375px width
- [ ] Modals dismiss with explicit close button
- [ ] Critical actions never hidden behind hover or right-click
- [ ] Sticky elements don't cover content
Accessibility test
- [ ] Semantic HTML (
<button>,<a>, headings in order) - [ ] All inputs have associated
<label> - [ ] Focus visible
- [ ] Color is not the only signal (errors include text + icon)
- [ ] Contrast meets WCAG AA (4.5:1 body, 3:1 large text)
- [ ] Icon-only buttons have
aria-label - [ ] Modals trap focus, restore on close
- [ ] Errors announced via
aria-liveorrole="alert" - [ ] No
onClickon<div>— use<button> - [ ] Skip-to-content link on long pages
Goodwill / common courtesy test
Krug's "reservoir of goodwill" — small UX choices that signal respect for the user.
- [ ] User input retained on error (don't wipe forms)
- [ ] Undo for destructive actions where possible
- [ ] Keyboard shortcuts disclosed (e.g.
?opens cheat sheet) - [ ] Loading is fast OR explicitly indicated; never silent
- [ ] No surprise modals, popups, or interruptions
- [ ] Errors don't blame the user ("You entered..." → "Email must contain @")
- [ ] No required fields without reason
- [ ] No multi-step wizard when one form would do
- [ ] Search and filter results show counts and active filters
- [ ] Time-sensitive info shows relative time (
5 min ago) not raw timestamps
---
Severity triage
When you find issues, prioritize:
| Severity | Definition | Examples |
|---|---|---|
| Blocker | User can't complete primary task | No primary CTA, broken form, error state with no recovery |
| High | Friction high enough to cause abandonment | Buried CTA, dead-end empty state, ambiguous destructive action |
| Medium | Slows the user but doesn't block | Verbose labels, missing breadcrumb, weak hierarchy |
| Low | Polish | Sentence case fixes, contractions, microcopy refinements |
Refactor blockers first. Don't waste a turn on Low issues if a Blocker exists.