$npx -y skills add dembrandt/dembrandt-skills --skill loading-states-and-perceived-performanceManage user expectations during wait times with appropriate loading states — from simple spinners to complex skeleton screens and staggered animations. Perceived performance is often more important than actual load time. Use when designing data-heavy components, handling API call
| 1 | # Loading States and Perceived Performance |
| 2 | |
| 3 | Users don't mind waiting as much if they understand *what* they are waiting for and *how much* progress is being made. Perceived performance is the design work of making a system feel faster than it actually is. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## Choosing the Right Loading State |
| 8 | |
| 9 | | Wait Duration | Best Pattern | Use for | |
| 10 | |---|---|---| |
| 11 | | **Short (< 1s)** | **Inline Spinner / Loader** | Button actions, small updates, quick data fetches | |
| 12 | | **Medium (1s – 3s)** | **Skeleton Screen** | Cards, lists, dashboards, profile pages | |
| 13 | | **Long (> 3s)** | **Determinate Progress Bar** | File uploads, complex exports, heavy processing | |
| 14 | | **Full Page** | **Staggered Entry / Animated Sections** | Initial app load, hero sections, immersive transitions | |
| 15 | |
| 16 | --- |
| 17 | |
| 18 | ## Simple Cases: Spinners and Loaders |
| 19 | |
| 20 | Use spinners for small, contained actions where the layout doesn't change significantly. |
| 21 | |
| 22 | - **Button Spinners:** Replace button text or sit alongside it. The button should enter a `disabled` state to prevent double-submissions. |
| 23 | - **Micro-Loaders:** A small 16–24px circle for inline updates (e.g., saving a single field). |
| 24 | - **Animation Tip:** A "spring-loaded" rotation (easing in and out) feels more premium than a constant linear rotation. |
| 25 | |
| 26 | ```css |
| 27 | @keyframes spin { |
| 28 | 0% { transform: rotate(0deg); } |
| 29 | 100% { transform: rotate(360deg); } |
| 30 | } |
| 31 | .spinner { |
| 32 | animation: spin 800ms cubic-bezier(0.4, 0, 0.2, 1) infinite; |
| 33 | } |
| 34 | ``` |
| 35 | |
| 36 | --- |
| 37 | |
| 38 | ## Skeleton Screens (Glimmer/Shimmer) |
| 39 | |
| 40 | Skeleton screens provide a visual placeholder that mimics the layout of the final content. This reduces "layout shift" (CLS) and signals to the user exactly where the content will appear. |
| 41 | |
| 42 | ### The Shimmer Effect |
| 43 | A subtle, moving gradient that travels across the skeleton elements. |
| 44 | |
| 45 | ```css |
| 46 | .skeleton { |
| 47 | background: var(--color-grey-100); |
| 48 | background-image: linear-gradient( |
| 49 | 90deg, |
| 50 | rgba(255, 255, 255, 0) 0%, |
| 51 | rgba(255, 255, 255, 0.5) 50%, |
| 52 | rgba(255, 255, 255, 0) 100% |
| 53 | ); |
| 54 | background-size: 200% 100%; |
| 55 | animation: shimmer 1.5s infinite; |
| 56 | } |
| 57 | |
| 58 | @keyframes shimmer { |
| 59 | 0% { background-position: -200% 0; } |
| 60 | 100% { background-position: 200% 0; } |
| 61 | } |
| 62 | ``` |
| 63 | |
| 64 | ### Rules for Skeletons |
| 65 | - **Match the shape:** If the final content is a round avatar, use a round skeleton. If it's a 2-line heading, use two bars of varying widths. |
| 66 | - **Stay Recessive:** Skeletons should use your most subtle grey (`--color-grey-100` or `grey-50`). They should not draw focus. |
| 67 | - **Fade into Reality:** When data arrives, fade the actual content in over the skeleton (150–200ms) rather than snapping. |
| 68 | |
| 69 | --- |
| 70 | |
| 71 | ## Fully Animated Sections |
| 72 | |
| 73 | For major page transitions or initial loads, use a coordinated animation strategy. |
| 74 | |
| 75 | ### Staggered Entry (Cascading) |
| 76 | Instead of the whole page appearing at once, animate sections in a sequence. This guides the user's eye from the most important content (hero) down to secondary areas. |
| 77 | |
| 78 | ```css |
| 79 | .section { |
| 80 | opacity: 0; |
| 81 | transform: translateY(10px); |
| 82 | animation: slide-up 400ms ease-out forwards; |
| 83 | } |
| 84 | /* Stagger by index */ |
| 85 | .section:nth-child(1) { animation-delay: 100ms; } |
| 86 | .section:nth-child(2) { animation-delay: 200ms; } |
| 87 | .section:nth-child(3) { animation-delay: 300ms; } |
| 88 | |
| 89 | @keyframes slide-up { |
| 90 | to { opacity: 1; transform: translateY(0); } |
| 91 | } |
| 92 | ``` |
| 93 | |
| 94 | ### Hero Section "Bloom" |
| 95 | For |