$npx -y skills add jamditis/claude-skills-journalism --skill web-ui-best-practicesSigns of taste in web UI. Use when building or reviewing any user-facing web interface — dashboards, SaaS apps, marketing sites, internal tools. Covers interaction speed, navigation depth, visual restraint, copy quality, and the small details that separate polished products from
| 1 | # Web UI best practices |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | ## Speed |
| 6 | |
| 7 | Every interaction completes in under 100ms. If it can't, fake it. |
| 8 | |
| 9 | - Optimistic UI updates — show the result before the server confirms |
| 10 | - Debounce inputs, but never debounce perceived response |
| 11 | - Prefetch likely next routes on hover or viewport entry |
| 12 | - Use `will-change` and `transform` for animations, never `top`/`left` |
| 13 | - Measure with `performance.now()`, not gut feel |
| 14 | |
| 15 | ```js |
| 16 | // Optimistic delete — remove from UI immediately, reconcile later |
| 17 | async function handleDelete(id) { |
| 18 | setItems(prev => prev.filter(i => i.id !== id)); |
| 19 | try { |
| 20 | await api.delete(`/items/${id}`); |
| 21 | } catch { |
| 22 | setItems(prev => [...prev, originalItem]); |
| 23 | toast("Couldn't delete. Restored."); |
| 24 | } |
| 25 | } |
| 26 | ``` |
| 27 | |
| 28 | ### Skeleton loading states |
| 29 | |
| 30 | 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. |
| 31 | |
| 32 | ```css |
| 33 | .skeleton { |
| 34 | background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); |
| 35 | background-size: 200% 100%; |
| 36 | animation: shimmer 1.5s infinite; |
| 37 | border-radius: 4px; |
| 38 | } |
| 39 | |
| 40 | @keyframes shimmer { |
| 41 | 0% { background-position: 200% 0; } |
| 42 | 100% { background-position: -200% 0; } |
| 43 | } |
| 44 | ``` |
| 45 | |
| 46 | ## Modern CSS toolkit |
| 47 | |
| 48 | Four capabilities matured between 2023 and 2026 that change how you build component-level responsive layouts and SPA-like transitions without JavaScript. Reach for them before adding a framework. |
| 49 | |
| 50 | ### Container queries |
| 51 | |
| 52 | Container queries let a component respond to **its container's** size, not the viewport's. The same card can render in a 300px sidebar and a 900px main column without media-query coordination at the page level. |
| 53 | |
| 54 | ```css |
| 55 | .card-list { |
| 56 | container-type: inline-size; |
| 57 | container-name: cards; |
| 58 | } |
| 59 | |
| 60 | @container cards (min-width: 480px) { |
| 61 | .card { display: grid; grid-template-columns: 120px 1fr; } |
| 62 | } |
| 63 | ``` |
| 64 | |
| 65 | Stable in all major browsers since 2023. Replaces most "the same component in two places needs to look different" hacks. |
| 66 | |
| 67 | ### `:has()` parent selector |
| 68 | |
| 69 | `:has()` lets a parent style itself based on its descendants — the long-requested "parent selector." Useful for marking a form field as in-error, a card as having an attached image, or a row as containing a focused input — all without JS. |
| 70 | |
| 71 | ```css |
| 72 | /* Highlight a form group when its input has focus */ |
| 73 | .form-group:has(input:focus) { |
| 74 | outline: 2px solid var(--color-primary); |
| 75 | } |
| 76 | |
| 77 | /* Add bottom margin to articles that contain a figure */ |
| 78 | article:has(figure) { |
| 79 | margin-bottom: 2rem; |
| 80 | } |
| 81 | ``` |
| 82 | |
| 83 | Stable in Chrome, Safari, and Firefox since late 2023. Cuts a real category of JS-driven class toggling. |
| 84 | |
| 85 | ### View transitions |
| 86 | |
| 87 | The View Transitions API animates between two DOM states (route changes, modal open/close, list-item swaps) without a framework. The browser snapshots the old state, swaps in the new state, then crossfades or slides between them. |
| 88 | |
| 89 | ```js |
| 90 | // Same-document transition (Chrome 111+, Safari TP, Firefox behind a flag) |
| 91 | function navigate(newView) { |
| 92 | if (!document.startViewTransition) { |
| 93 | renderView(newView); |
| 94 | return; |
| 95 | } |
| 96 | document.startViewTransition(() => renderView(newView)); |
| 97 | } |
| 98 | ``` |
| 99 | |
| 100 | ```css |
| 101 | /* Smooth crossfade by default; override per element */ |
| 102 | ::view-transition-old(*) { animation-duration: 200ms; } |
| 103 | ::view-transition-new(*) { animation-duration: 200ms; } |
| 104 | ``` |
| 105 | |
| 106 | Cross-document view transitions (between full page navigations) shipped to Chrome 126 in 2024 and let MPAs feel like SPAs. Pair with `prefers-reduced-motion` so users with motion sensitivity get an instant swap, not an animation. |
| 107 | |
| 108 | ### Scroll-driven animations |
| 109 | |
| 110 | `animation-timeline: scroll()` and `animation-timeline: view()` drive CSS animations from scroll position instead of wall-clock time. The classic use case is a progress indicator at the top of an article that fills as you scroll. |
| 111 | |
| 112 | ```css |
| 113 | @keyframes fill { from { transform: scaleX(0); } to { transform: scaleX(1); } } |
| 114 | |
| 115 | .read-progress { |
| 116 | position: fixed; top: 0; left: 0; right: 0; height: 3px; |
| 117 | background: var(--color-primary); |
| 118 | transform-origin: left; |
| 119 | animation: fill linear; |
| 120 | animation-timeline: scroll(root); |
| 121 | } |
| 122 | ``` |
| 123 | |
| 124 | Stable in Chromium-based browsers (Chrome 115+, Edge); not yet in Safari or Firefox as of 2026-05. Use as progressive enhancement; provide a JS fallback or accept a less-flashy baseline elsewhere. |
| 125 | |
| 126 | ## No product tours |
| 127 | |
| 128 | If you need a tour to explain your UI, the UI is wrong. Instead: |
| 129 | |
| 130 | - Empty states that teach by doing ("Create your first project") |
| 131 | - Progressive disclosure — show features w |