web-ui-best-practices

SKILL.md

Web UI best practices

Principles for building web interfaces that feel fast, intentional, and respectful of the user's time. Every rule here is a smell test — violating one is fine if you have a reason, violating several means the UI needs work.

Speed

Every interaction completes in under 100ms. If it can't, fake it.

  • Optimistic UI updates — show the result before the server confirms
  • Debounce inputs, but never debounce perceived response
  • Prefetch likely next routes on hover or viewport entry
  • Use will-change and transform for animations, never top/left
  • Measure with performance.now(), not gut feel
// Optimistic delete — remove from UI immediately, reconcile later
async function handleDelete(id) {
  setItems(prev => prev.filter(i => i.id !== id));
  try {
    await api.delete(`/items/${id}`);
  } catch {
    setItems(prev => [...prev, originalItem]);
    toast("Couldn't delete. Restored.");
  }
}

Skeleton loading states

Never show a spinner when you know the shape of what's coming. Render a skeleton that matches the layout, then swap in real content.

.skeleton {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 4px;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

No product tours

If you need a tour to explain your UI, the UI is wrong. Instead:

  • Empty states that teach by doing ("Create your first project")
  • Progressive disclosure — show features when they become relevant
  • Inline hints that disappear after first use
  • Defaults that work without configuration

URLs

Slugs are short, readable, and human-guessable. No UUIDs, no query param soup.

Good:  /projects/weather-app
       /settings/billing
       /docs/api/auth

Bad:   /projects/550e8400-e29b-41d4-a716-446655440000
       /app?view=settings&tab=billing&subsection=plan
       /dashboard#!/module/documents/list?filter=active
  • Use slugs derived from user-provided names
  • Keep nesting to 3 segments max
  • Make URLs copyable and shareable — they are the product's memory

Persistent resumable state

Users leave and come back. Respect that.

  • Save draft form state to localStorage or the server
  • Restore scroll position on back navigation
  • Preserve filter/sort selections across sessions
  • URL encodes the current view state — sharing a URL reproduces the view
// Persist form state across sessions
function usePersistentForm(key, defaults) {
  const [state, setState] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : defaults;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(state));
  }, [key, state]);

  return [state, setState];
}

Color restraint

Not more than 3 colors. One primary, one accent, one for danger/destructive. Everything else is shades of gray.

:root {
  --color-primary: #2563eb;
  --color-accent: #f59e0b;
  --color-danger: #ef4444;

  --gray-50: #fafafa;
  --gray-100: #f4f4f5;
  --gray-200: #e4e4e7;
  --gray-400: #a1a1aa;
  --gray-600: #52525b;
  --gray-900: #18181b;
}
  • Use opacity and lightness to create hierarchy, not new hues
  • Dark mode is the same 3 colors with inverted grays
  • If you reach for a 4th color, you're compensating for weak layout

No visible scrollbars

Hide them unless the user is actively scrolling. Content feels infinite, not trapped.

/* Hide scrollbar across browsers */
.scroll-container {
  overflow-y: auto;
  scrollbar-width: none;          /* Firefox */
  -ms-overflow-style: none;       /* IE/Edge */
}
.scroll-container::-webkit-scrollbar {
  display: none;                  /* Chrome/Safari */
}

Use scroll shadows to hint at overflow without chrome:

.scroll-shadow {
  background:
    linear-gradient(white 30%, transparent),
    linear-gradient(transparent, white 70%) 0 100%,
    radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.15), transparent),
    radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.15), transparent) 0 100%;
  background-repeat: no-repeat;
  background-size: 100% 40px, 100% 40px, 100% 12px, 100% 12px;
  background-attachment: local, local, scroll, scroll;
}

Navigation depth

All navigation is 3 steps or fewer from anywhere. If the user needs more than 3 clicks to reach a destination, flatten the hierarchy.

  • Breadcrumbs for depth, not for navigation
  • Global nav always visible, never hidden behind a hamburger on desktop
  • Use Cmd+K / Ctrl+K as the escape hatch for power users

Command palette (Cmd+K)

Every app with more than one page needs a command palette.

// Minimal Cmd+K listener
useEffect(() => {
  function handleKeyDown(e) {
    if ((e.metaKey || e.ctrlKey) && e.key === "k") {
      e.preventDefault();
      setCommandPaletteOpen(true);
    }
  }
  document.addEventListener("keydown", handleKeyDown);
  return () => document.removeEventListener("keydown", handleKeyDown);
}, []);

Keep the palette simple:

  • Fuzzy search over page names, recent actions, settings
  • Show keyboard shortcuts inline
  • Most recent items first
  • No categories until you have 20+ commands

Clipboard

Copy and paste should work everywhere the user expects it.

  • One-click copy on codes, URLs, API keys, IDs
  • Paste from clipboard into file uploads, image fields
  • Show brief confirmation on copy ("Copied!") that auto-dismisses
async function copyToClipboard(text, label = "Copied") {
  await navigator.clipboard.writeText(text);
  toast(label, { duration: 1500 });
}

Hit targets

Larger hit targets for buttons and inputs. Minimum 44x44px touch targets (WCAG 2.5.8). On desktop, generous padding is still faster than precise aim.

button, .btn, [role="button"] {
  min-height: 44px;
  min-width: 44px;
  padding: 10px 20px;
}

input, select, textarea {
  min-height: 44px;
  padding: 10px 12px;
  font-size: 16px;  /* Prevents iOS zoom on focus */
}
  • Adjacent clickable elements need at least 8px gap
  • Icon-only buttons get larger padding than labeled buttons
  • Don't rely on hover states for critical affordances — they don't exist on touch

Honest cancellation

One-click cancel. No guilt trips, no dark patterns, no "Are you sure you want to miss out?"

  • Cancel button is always visible alongside confirm
  • Account deletion works on the first try
  • Unsubscribe is one click, not a preference center
  • Downgrade flows don't require contacting support

Tooltips

Very minimal. Tooltips are a confession that the UI doesn't speak for itself.

  • Only on icon-only buttons (to provide the label)
  • Never on text that's already readable
  • Show on hover after 300ms delay, not instantly
  • Dismiss on scroll
  • Never use tooltips for essential information

Copy

Active voice. Max 7 words per sentence. Talk like a person, not a legal document.

Good:  "Project created"
       "Saved 2 minutes ago"
       "Delete this file?"

Bad:   "Your project has been successfully created!"
       "Changes were last saved approximately 2 minutes ago"
       "Are you sure you want to permanently delete this file? This action cannot be undone."
  • Buttons are verbs: "Save", "Delete", "Send" — not "Submit", "OK", "Confirm"
  • Error messages say what happened and what to do next
  • Never blame the user ("Invalid input" → "Enter a valid email")
  • Use sentence case everywhere, never Title Case in UI copy

Optical alignment

Optical alignment over geometric alignment. The eye doesn't see pixels, it sees weight.

  • Play icons shift 2-3px right inside circles to look centered
  • Text with leading capital letters aligns optically left of its bounding box
  • Icons next to text need 1-2px vertical offset depending on the glyph
  • Padding around text is visually balanced, not mathematically equal — bottom padding is often 1-2px more than top
/* Geometric center ≠ optical center */
.play-button svg {
  transform: translateX(2px);
}

/* Visually balanced card padding */
.card {
  padding: 20px 24px 22px 24px;
}

Left-to-right reading flow

Optimized for L-to-R reading and the F-pattern scan.

  • Most important content in the top-left quadrant
  • Primary actions on the right (where the eye ends a line)
  • Labels above inputs, not beside them
  • Tables: most-scanned column is leftmost
  • Don't center-align body text — left-align everything except single-line headings

Reassurance about loss

Users fear losing work. Prevent it and prove it.

  • Auto-save with visible "Saved" indicator and timestamp
  • Undo after destructive actions (soft delete, not hard delete)
  • "You have unsaved changes" on navigation away
  • Version history for anything longer than a tweet
  • Confirmation only for irreversible actions, not routine ones
// Warn on unsaved changes
useEffect(() => {
  function handleBeforeUnload(e) {
    if (hasUnsavedChanges) {
      e.preventDefault();
      e.returnValue = "";
    }
  }
  window.addEventListener("beforeunload", handleBeforeUnload);
  return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [hasUnsavedChanges]);

Copyable brand assets

Ship a /brand or /press page with a downloadable SVG logo and brand kit. Don't make people screenshot your logo.

  • SVG logo with transparent background
  • Color codes (hex, RGB, HSL)
  • Font names and weights
  • Usage guidelines (minimum size, clear space, don'ts)
  • One-click download as ZIP

Checklist

Use this when reviewing any web UI:

  • Every interaction under 100ms (or optimistically faked)
  • No product tour or onboarding modal
  • URLs are short, readable, no UUIDs
  • State persists across sessions and page reloads
  • 3 colors max (plus grays)
  • No visible scrollbars at rest
  • Any destination reachable in 3 steps or fewer
  • SVG logo and brand kit downloadable
  • Skeleton loaders, not spinners
  • Clipboard copy works on codes, keys, URLs
  • Touch targets 44px minimum
  • Cancel is honest and one-click
  • Cmd+K command palette exists
  • Tooltips only on icon-only buttons
  • Copy is active voice, 7 words max
  • Optical alignment, not geometric
  • Content follows L-to-R F-pattern
  • Auto-save with visible status and undo
Weekly Installs
14
GitHub Stars
56
First Seen
14 days ago
Installed on
github-copilot14
codex14
kimi-cli14
gemini-cli14
cursor14
amp14