{"course":{"slug":"motion-and-audio","title":"Motion, Sound and Music","url":"https://addaly.com/learn/motion-and-audio","level":"intermediate","track":"create"},"lesson":{"slug":"waiting-states","title":"Spinners, skeletons and honest waiting","url":"https://addaly.com/learn/motion-and-audio/waiting-states","position":16,"of":72,"minutes":9,"answer":"Delay the indicator by 200 to 300ms so fast responses never flash a spinner, use an indeterminate indicator whenever you do not genuinely know the progress.","keyIdea":"Delay the indicator by 200 to 300ms so fast responses never flash a spinner, use an indeterminate indicator whenever you do not genuinely know the progress, and treat a skeleton as a promise about layout that will cost you twice if it is wrong."},"body":{"format":"markdown","text":"## The indicator is a claim, and it can be false\n\nA spinner says: something is happening, and it will finish. Both halves of that are claims about your system, and both are frequently untrue. A spinner that keeps turning after the request has failed is lying, and users learn fast that spinners lie, which is why people reload pages that were about to succeed.\n\nDesigning a waiting state starts with the duration, not the graphic.\n\n## Three bands\n\n**Under about 100ms.** Show nothing. The response arrives inside the window where the action still feels instantaneous, and a spinner that appears and disappears within 100ms is a visible flash that makes a fast interaction feel broken. If you cannot know the duration in advance, delay the indicator: show nothing for the first 200 to 300ms, then show the spinner if the work is still running. Most people are surprised by how much this alone improves a product's perceived speed: you have removed flicker from every fast case and lost nothing on the slow ones.\n\n**Roughly 100ms to 1 second.** Keep the user's context on screen and indicate locally. A button that shows a small inline spinner in place of its label. A section that dims. Do not blank the screen: replacing a full page of content with a centred spinner for 400ms is a worse experience than leaving the stale content visible with a subtle busy state.\n\n**Beyond 1 second.** Now you need a real indicator, and past roughly 4 or 5 seconds you need one that shows progress or explains itself. Past 10 seconds, the interaction is over as far as attention is concerned: the user has switched tasks, and your job is to be findable when they come back, which means a persistent result rather than a modal they have to be present for.\n\n## Determinate, indeterminate, and the honest choice\n\nA **determinate** progress bar claims you know how far along you are. Use it only when you do: a file upload knows its byte count, a multi-step job knows its steps.\n\nAn **indeterminate** indicator (the looping bar, the spinner) claims only that work is happening. It is the right choice far more often than it is used, because most web requests have unknown duration.\n\nThe failure people remember is a determinate bar that stalls at 90 per cent. Perceived duration research is consistent on this: a progress bar that decelerates toward the end feels much longer than one of identical total duration that accelerates. If you must fake it, fake it in the direction of speeding up. The more defensible answer is to stop faking and use an indeterminate indicator, which makes no promise it cannot keep.\n\n## Skeleton screens: useful and frequently misused\n\nA skeleton screen shows grey blocks in the shape of the content that is about to arrive. It works for two reasons: the layout does not jump when content lands, and the user can start orienting before the data exists.\n\nIt stops working when the skeleton does not match. If your skeleton shows three cards and four arrive, the page re-flows at exactly the moment the user started reading: you have replaced one jolt with two. A skeleton is a promise about layout, and a wrong promise is worse than a spinner.\n\nThe shimmer that sweeps across a skeleton is doing one honest job: distinguishing \"loading\" from \"empty\". Without it, grey blocks can read as a broken render. Keep it slow (a 1 to 1.5 second sweep), and remember that it is continuous motion, so it belongs inside your reduced-motion handling.\n\n## Optimistic updates, and what they cost\n\nThe fastest waiting state is no waiting state: apply the change locally, send the request, and reconcile afterwards. A \"like\" button should fill instantly.\n\nThis is correct for actions that almost always succeed and are cheap to reverse. It is wrong for anything where a silent rollback would mislead. If a payment optimistically shows as sent and then quietly reverts, you have built something worse than slow. The test: if this fails and the user has already moved on, what do they believe?\n\n## Measuring rather than guessing\n\nDo not design waiting states against your own network. Throttle to a slow connection in DevTools and watch what your users see: which parts of the page arrive first, how long the gap is, whether the layout settles once or four times.\n\n`ffmpeg -i capture.mp4 -vf fps=60 f_%04d.png` on a screen recording lets you count frames from click to first content. Chrome's Performance panel gives you Largest Contentful Paint and Cumulative Layout Shift directly, both free, and CLS in particular is the number that catches a mismatched skeleton.\n\n## The free path\n\nEverything here is free. CSS animations for spinners and shimmers need no library. The `<progress>` element is native. For skeletons, a handful of divs with a background gradient and one keyframe animation is the entire implementation; the packaged libraries exist to save fifteen minutes, not to do anything you cannot.\n\n## The limitation\n\nNo waiting state makes waiting shorter. There is a real, measurable effect from good indicators (people report shorter perceived durations and abandon less) but it is worth a few seconds at most, and it does not compound. If your page takes eight seconds, the honest fix is in the eight seconds, not in the animation covering them.\n\nThere is also a documented perverse case: adding an artificial delay with a progress animation can make a result feel more trustworthy, which is why some products slow themselves down deliberately. That works, and it is manipulation. Knowing the mechanism is the point; whether you use it that way is a decision you should make out loud rather than by accident."},"check":{"question":"A team adds a skeleton screen to a feed. Load feels smoother in testing, but the page's Cumulative Layout Shift score gets worse. What has most likely happened?","options":[{"text":"The shimmer animation on the skeleton is being counted as layout movement, because any animated element contributes to the layout shift metric while it is in motion.","correct":false,"tempting":"Animated transforms are explicitly excluded from layout shift scoring; the metric measures unexpected movement of laid-out content."},{"text":"Skeletons always increase layout shift because they add a render pass, and the correct trade is to accept a worse score for a better perceived experience.","correct":false,"tempting":"Treats a measurable regression as an inherent cost of the technique rather than as a sign the skeleton does not match."},{"text":"The skeleton's shape does not match the content that arrives, so the page re-flows when real data lands: replacing one jolt with two.","correct":true},{"text":"The skeleton is rendered before the fonts have loaded, so the metric is capturing the font swap rather than the skeleton, and the fix is font-display rather than anything to do with the loading state.","correct":false,"tempting":"A real source of layout shift, but it would have existed before the skeleton was added and cannot explain a change that appeared with it."}]},"prev":{"slug":"motion-tokens","title":"Naming the motion so it stops drifting","url":"https://addaly.com/learn/motion-and-audio/motion-tokens"},"next":{"slug":"prefers-reduced-motion","title":"Motion that makes people ill","url":"https://addaly.com/learn/motion-and-audio/prefers-reduced-motion"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Spinners, skeletons and honest waiting', in Motion, Sound and Music. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/motion-and-audio/waiting-states (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Spinners, skeletons and honest waiting","courseTitle":"Motion, Sound and Music","module":{"index":2,"slug":"motion-that-carries-meaning","title":"Motion that carries meaning"},"answer":null,"keyIdea":"Delay the indicator by 200 to 300ms so fast responses never flash a spinner, use an indeterminate indicator whenever you do not genuinely know the progress, and treat a skeleton as a promise about layout that will cost you twice if it is wrong.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"the-indicator-is-a-claim-and-it-can-be-false","runs":[{"text":"The indicator is a claim, and it can be false"}]},{"t":"p","runs":[{"text":"A spinner says: something is happening, and it will finish. Both halves of that are claims about your system, and both are frequently untrue. A spinner that keeps turning after the request has failed is lying, and users learn fast that spinners lie, which is why people reload pages that were about to succeed."}]},{"t":"p","runs":[{"text":"Designing a waiting state starts with the duration, not the graphic."}]},{"t":"h2","id":"three-bands","runs":[{"text":"Three bands"}]},{"t":"p","runs":[{"text":"Under about 100ms.","b":true},{"text":" Show nothing. The response arrives inside the window where the action still feels instantaneous, and a spinner that appears and disappears within 100ms is a visible flash that makes a fast interaction feel broken. If you cannot know the duration in advance, delay the indicator: show nothing for the first 200 to 300ms, then show the spinner if the work is still running. Most people are surprised by how much this alone improves a product’s perceived speed: you have removed flicker from every fast case and lost nothing on the slow ones."}]},{"t":"p","runs":[{"text":"Roughly 100ms to 1 second.","b":true},{"text":" Keep the user’s context on screen and indicate locally. A button that shows a small inline spinner in place of its label. A section that dims. Do not blank the screen: replacing a full page of content with a centred spinner for 400ms is a worse experience than leaving the stale content visible with a subtle busy state."}]},{"t":"p","runs":[{"text":"Beyond 1 second.","b":true},{"text":" Now you need a real indicator, and past roughly 4 or 5 seconds you need one that shows progress or explains itself. Past 10 seconds, the interaction is over as far as attention is concerned: the user has switched tasks, and your job is to be findable when they come back, which means a persistent result rather than a modal they have to be present for."}]},{"t":"h2","id":"determinate-indeterminate-and-the-honest-choice","runs":[{"text":"Determinate, indeterminate, and the honest choice"}]},{"t":"p","runs":[{"text":"A "},{"text":"determinate","b":true},{"text":" progress bar claims you know how far along you are. Use it only when you do: a file upload knows its byte count, a multi-step job knows its steps."}]},{"t":"p","runs":[{"text":"An "},{"text":"indeterminate","b":true},{"text":" indicator (the looping bar, the spinner) claims only that work is happening. It is the right choice far more often than it is used, because most web requests have unknown duration."}]},{"t":"p","runs":[{"text":"The failure people remember is a determinate bar that stalls at 90 per cent. Perceived duration research is consistent on this: a progress bar that decelerates toward the end feels much longer than one of identical total duration that accelerates. If you must fake it, fake it in the direction of speeding up. The more defensible answer is to stop faking and use an indeterminate indicator, which makes no promise it cannot keep."}]},{"t":"h2","id":"skeleton-screens-useful-and-frequently-misused","runs":[{"text":"Skeleton screens: useful and frequently misused"}]},{"t":"p","runs":[{"text":"A skeleton screen shows grey blocks in the shape of the content that is about to arrive. It works for two reasons: the layout does not jump when content lands, and the user can start orienting before the data exists."}]},{"t":"p","runs":[{"text":"It stops working when the skeleton does not match. If your skeleton shows three cards and four arrive, the page re-flows at exactly the moment the user started reading: you have replaced one jolt with two. A skeleton is a promise about layout, and a wrong promise is worse than a spinner."}]},{"t":"p","runs":[{"text":"The shimmer that sweeps across a skeleton is doing one honest job: distinguishing “loading” from “empty”. Without it, grey blocks can read as a broken render. Keep it slow (a 1 to 1.5 second sweep), and remember that it is continuous motion, so it belongs inside your reduced-motion handling."}]},{"t":"h2","id":"optimistic-updates-and-what-they-cost","runs":[{"text":"Optimistic updates, and what they cost"}]},{"t":"p","runs":[{"text":"The fastest waiting state is no waiting state: apply the change locally, send the request, and reconcile afterwards. A “like” button should fill instantly."}]},{"t":"p","runs":[{"text":"This is correct for actions that almost always succeed and are cheap to reverse. It is wrong for anything where a silent rollback would mislead. If a payment optimistically shows as sent and then quietly reverts, you have built something worse than slow. The test: if this fails and the user has already moved on, what do they believe?"}]},{"t":"h2","id":"measuring-rather-than-guessing","runs":[{"text":"Measuring rather than guessing"}]},{"t":"p","runs":[{"text":"Do not design waiting states against your own network. Throttle to a slow connection in DevTools and watch what your users see: which parts of the page arrive first, how long the gap is, whether the layout settles once or four times."}]},{"t":"p","runs":[{"text":"ffmpeg -i capture.mp4 -vf fps=60 f_%04d.png","code":true},{"text":" on a screen recording lets you count frames from click to first content. Chrome’s Performance panel gives you Largest Contentful Paint and Cumulative Layout Shift directly, both free, and CLS in particular is the number that catches a mismatched skeleton."}]},{"t":"h2","id":"the-free-path","runs":[{"text":"The free path"}],"role":"free"},{"t":"p","runs":[{"text":"Everything here is free. CSS animations for spinners and shimmers need no library. The "},{"text":"<progress>","code":true},{"text":" element is native. For skeletons, a handful of divs with a background gradient and one keyframe animation is the entire implementation; the packaged libraries exist to save fifteen minutes, not to do anything you cannot."}]},{"t":"h2","id":"the-limitation","runs":[{"text":"The limitation"}],"role":"limit"},{"t":"p","runs":[{"text":"No waiting state makes waiting shorter. There is a real, measurable effect from good indicators (people report shorter perceived durations and abandon less) but it is worth a few seconds at most, and it does not compound. If your page takes eight seconds, the honest fix is in the eight seconds, not in the animation covering them."}]},{"t":"p","runs":[{"text":"There is also a documented perverse case: adding an artificial delay with a progress animation can make a result feel more trustworthy, which is why some products slow themselves down deliberately. That works, and it is manipulation. Knowing the mechanism is the point; whether you use it that way is a decision you should make out loud rather than by accident."}]}],"check":{"question":[{"text":"A team adds a skeleton screen to a feed. Load feels smoother in testing, but the page’s Cumulative Layout Shift score gets worse. What has most likely happened?"}],"options":[{"text":[{"text":"The shimmer animation on the skeleton is being counted as layout movement, because any animated element contributes to the layout shift metric while it is in motion."}],"correct":false,"tempting":[{"text":"Animated transforms are explicitly excluded from layout shift scoring; the metric measures unexpected movement of laid-out content."}]},{"text":[{"text":"Skeletons always increase layout shift because they add a render pass, and the correct trade is to accept a worse score for a better perceived experience."}],"correct":false,"tempting":[{"text":"Treats a measurable regression as an inherent cost of the technique rather than as a sign the skeleton does not match."}]},{"text":[{"text":"The skeleton’s shape does not match the content that arrives, so the page re-flows when real data lands: replacing one jolt with two."}],"correct":true,"tempting":null},{"text":[{"text":"The skeleton is rendered before the fonts have loaded, so the metric is capturing the font swap rather than the skeleton, and the fix is font-display rather than anything to do with the loading state."}],"correct":false,"tempting":[{"text":"A real source of layout shift, but it would have existed before the skeleton was added and cannot explain a change that appeared with it."}]}],"order":[0,3,2,1],"why":null},"prev":{"slug":"motion-tokens","title":"Naming the motion so it stops drifting","minutes":8},"next":{"slug":"prefers-reduced-motion","title":"Motion that makes people ill","minutes":10}}}