Don't Leave Users Staring at a Spinner: Loading and Progress Feedback for Vibe Projects
Waiting hurts mostly because of uncertainty, not time. This guide covers skeleton screens vs spinners vs progress bars, the missing 'submitting' button state, background processing, optimistic updates, upload progress, and timeout handling.

You tap a button and the screen does nothing for seconds. That is one of the most anxious moments in any product. The user cannot tell whether the tap registered, whether work is running, or whether everything froze — so they tap again, go back, re-enter, and turn an operation that would have succeeded into a failure.
This guide is about those seconds: how to design loading states, how to show progress, and when it is acceptable to "pretend to succeed first." The example product "Riverside Reading Room" (a small app for reading together and swapping books) and the person "Akui" are fictional. The methods apply to any vibe project.
First answer one question: what is the user waiting for?
The first step of loading design is not choosing a spinner or a skeleton screen. It is saying clearly what the user is waiting for. The wait differs, and so does the design: waiting for a list suits a skeleton screen; waiting for a submit suits a button that turns into "Submitting…" and disables itself; waiting for a large upload suits a progress bar with a percentage; waiting for an AI generation of unknown length suits step-by-step status text — "Reading your book list… Finding similar books…"
The worst approach is one full-screen spinner for every wait. The user sees no information and can only wait; past three seconds, anxiety starts; past ten seconds, most people leave. But as long as users know "the system is working, how far it has got, and roughly how much longer," the same ten seconds become much easier. Most of the pain of waiting comes from uncertainty, not from time itself.
Skeleton screens, spinners, progress bars: when to use which
Skeleton screens suit content pages: lists, detail pages, profiles. Grey blocks hold the places first — "an image here, three lines of text there" — and real content replaces them block by block. The benefit is no layout jump: users do not tap the wrong thing because content suddenly pushed the page down. But skeleton screens have a flaw: used in the wrong place, they mislead. If an operation has unknown duration and no fixed content shape, a forced skeleton looks fake; an honest spinner with a sentence is better.
Spinners suit short waits: a refresh, a check, a small submit. A spinner must come with a sentence saying what is happening: "Saving…" or "Checking your book list…" A spinner with no words still leaves users guessing what it is doing and how long it will take. A spinner past five seconds must be upgraded: switch to a more specific status message, or offer "Continue in the background — we will notify you when it is done." Never make users stare at a spinning circle indefinitely.
Progress bars suit things that can be measured: uploads, downloads, batch jobs. Two practices ruin progress bars: fake progress — stuck at 90% for ages, which feels like being lied to — and jumping — from 10% straight to 100%, which makes users doubt any work happened at all. Be honest: if a percentage can be computed, show it; if not, show step counts ("Step 2 of 4"); if neither can be shown, skip the bar and rotate status copy instead.
The "submitting" button state is the easiest to forget
Many vibe projects give buttons only two looks: tappable and disabled. After tapping, there is no "submitting" state, so users assume the tap missed and tap three times — and the same book gets swapped three times, the same comment posted three times. The fix: the moment it is tapped, the button becomes "Submitting…" and disables itself; on success it returns to normal or navigates on, on failure it returns to normal with an error. That change is under ten lines of code, yet it kills a whole class of duplicate-submission complaints.
Akui hit this exact trap in Riverside Reading Room: a book-swap request had no submitting state, users tapped repeatedly, and the system recorded three duplicate requests — the other reader thought she was spamming. The fix was those two moves: disable plus a copy change. One extra note: while disabled, the button should not vanish into grey nothingness. Keep its position, change only the copy and style, so the page does not jump and users know "that tap was received."
Long waits should grant the right to walk away
Some operations are bound to take a long time: importing a book list in bulk, generating an annual reading report, waiting for another reader to confirm a swap. The kindest design here is not a prettier progress bar, but permission to walk away: tap "Process in the background, notify me when done," keep browsing elsewhere, and get a message or badge when it finishes. Forcing users to watch a progress bar for three minutes is a quiet form of disrespect.
When granting that right, two things must be stated clearly. First, will the task keep running after they leave — say so explicitly: "You can browse on; processing will not stop." Second, where do they find the result when they return — give a fixed entry point, like a "Processing" list or a notification center. Do not make them come back to find nothing and submit again. Riverside Reading Room's annual report was fixed exactly this way: users used to have to guard the page; now one tap lets them browse book lists, and a message arrives when the report is ready. Duplicate submissions dropped to zero.
Optimistic updates: pretend success first, but keep a way back
Some actions can "pretend to succeed first": tapping save turns the star yellow instantly; tapping like increments the count instantly; posting a comment puts it at the top of the list instantly. The request is still running in the background, but the interface shows success first, and it feels fast. This is called an optimistic update, a technique big products use constantly, and vibe projects can use it too.
But optimistic updates have a precondition: when they fail, the interface must be able to take it back. If a save request ultimately fails, the star should quietly revert, with a light note: "Save did not go through — tap to retry." If a comment fails to send, it must not vanish; it should stay in place marked "Send failed — tap to resend." The worst sin is pretending success and then going silent on failure. A user who believes they saved something, then finds it missing on another device, feels deceived — far worse than a half-second delay.
Whether optimistic updates are safe comes down to a simple test: how likely is failure, and how bad is it if it fails? Low-risk actions like likes and saves can go optimistic; high-risk actions like payments, sign-ups, and deletions should wait for the result before showing success. The higher the risk, the less you should run ahead.
Draw a line between load failure and an empty page
When loading fails, the worst presentation is an empty state. The network dropped, but the page says "No books yet" — users conclude the reading room is genuinely empty. This was covered in the companion guide on errors and empty states; the operational point here is: a failed load must do three things at once — say plainly that the connection failed (not that there is no data), offer a retry button, and keep the last successful content on screen if any exists. A book list that fails to refresh can show "This refresh did not come through — retry" on top, keep the cached list below, and label it "Content from last time." With old content as padding, users will not feel the whole product broke.
There is also the common partial failure: the list loaded, but all the cover images failed. Do not error the whole page for that — handle the failed part locally: a placeholder image, with "Image failed to load — tap to retry" on the image slot, or retry when opening the large view. Handle local failures locally; reserve full-page errors for full-page failures. That sense of proportion makes a product feel stable.
Writing progress copy: specific, honest, rhythmic
"Loading…" is the laziest copy there is; it says nothing. Better copy says what is being done: "Finding books close to your taste…" or "Checking the other reader's shelf…" Better still, break the steps out and rotate them: "Reading your book list (1/3) → Matching similar books (2/3) → Writing up why we recommend them (3/3)." The more concrete the steps, the more users feel "it is working" rather than "it is stuck."
But step copy has its own trap: do not invent steps that do not exist. If the system really has one step, splitting it into five to "look busy" backfires the moment a user notices one step lingering — trust drops faster. Honest rhythm is: as many steps as there really are, roughly even in duration; if one step is genuinely slow, make its copy more specific, for example: "Waiting for the other reader's confirmation — this step can take a few minutes, feel free to browse meanwhile."
Skeleton screens: position, count, duration
Skeleton screens are not just a few grey blocks thrown on the page; three details decide whether they feel real. First, position: grey blocks should roughly match where real content will land — image on top, title below — so replacement happens without a jump. If positions mismatch, the swap flashes and feels messier. Second, count: show about as many grey rows as one screen of real content would hold. Ten grey rows that resolve into two real items make users feel something got lost. Third, duration: skeleton screens suit one- or two-second waits. Past three seconds with nothing back, switch to a more specific message — "This one is slow, still working on it" — rather than letting grey blocks shimmer forever. Endlessly shimmering grey blocks breed the same suspicion as an endless spinner: is it dead?
Another misuse of skeleton screens: form submits. A user taps submit and the whole form turns into shimmering grey — they will think the page was cleared and their input lost. The correct feedback for a submit is always the button itself changing (submitting plus disabled), never swapping the whole page for a skeleton. Skeleton screens are for "looking at content," not for "doing an action."
Upload progress deserves the most care
Uploads are where users feel progress most, and where it most often goes wrong. First, put numbers next to the bar: percentage, uploaded size, total size — at least two of the three. "Uploaded 3.2 MB of 8 MB (40%)" is far more credible than a bare line. Second, allow canceling during upload, but confirm once before canceling — a user who accidentally cancels at 90% after a long upload will be devastated. Third, a failed upload should support resuming or one-tap re-upload, not force users to re-pick the file from scratch; if true resume is technically out of reach, at least keep the chosen file in place so they do not have to dig through folders again.
Multi-file uploads need one more layer: overall progress and per-file progress shown separately. Overall says "3 of 5 files done"; per-file says "this one is at 60%." If one file fails, mark only that one red and give it its own retry — do not send the whole batch back because of one file. Users' patience for uploads depends on whether they feel "the system is saving me effort." Designs that save effort earn waiting time.
Search and filter waits: do not leave users staring at blank space
Search is the most common "wait a moment" scenario, and its handling is often neglected. A user types a keyword, hits search, the list goes blank, and results appear two seconds later — those two blank seconds are easily misread as "nothing found." The right way: after the search fires, keep the old results with a light "Searching…" hint, or switch to a skeleton screen; replace only when results arrive. While old results are shown, label them "Previous results" so nobody mistakes them for new ones.
Filters work the same way: every filter change re-fetches the list, and each flicker makes the user's heart skip. For small changes (like switching sort order), re-sort the data already on hand locally and show it instantly, then verify in the background; for big changes, use a skeleton screen. One more trick: do not fire a request on every keystroke in the search box — wait for a half-second pause, or wait for the search tap. Otherwise requests fly everywhere, and a slow one can return after a newer one and overwrite fresh results with stale ones — the strangest search bug there is, where results visibly "go backwards."
After a timeout: give users an explanation, not a spinner
Every wait needs a ceiling. Past a reasonable time (usually 10 to 30 seconds depending on the action), stop, say plainly "we did not get a result this time," and offer choices: retry, try later, or go do something else first. An endless wait is the worst design of all — users cannot tell whether to keep waiting or give up, and usually end up refreshing, which risks duplicate submits or lost data.
Timeout copy should also split by cause: network problems say network problems; no response from the other side says no response; queued work says queued. Riverside Reading Room's swap confirmation once uniformly said "Request timed out," and users assumed their own connection was at fault and reconnected repeatedly. Changing it to "The other reader has not responded yet — browse meanwhile, we will notify you when they do" cut complaints immediately. A timeout is not a synonym for failure — often it just means "not finished yet." Saying "not finished" and "failed" apart tells users whether to wait or to act.
Motion rhythm: fast, steady, never the star
Loading motion itself has a rhythm to get right. Skeleton shimmer should be soft and slow; fast flicker strains the eyes. Spinner speed should be even; speeding up and slowing down makes the system feel like it is "struggling." Progress bars should advance smoothly — even if real progress jumps, smooth the display a little instead of letting it hop block by block. Motion is a supporting actor whose job is to say "still alive," not to perform for the audience.
One group is easily forgotten: motion-sensitive users. Rapid flashing and large moving loading animations are genuinely uncomfortable for them. If the product has a "reduce motion" switch, loading motion must follow it: with reduced motion on, skeleton screens become static grey blocks and spinners become static text saying "Loading." Consideration taken this far is what makes a product feel steadier than most of its peers.
Finally, one test to run before launch: the "waiting grand tour." List every place in the product where waiting happens — opening a page, refreshing, submitting, uploading, searching, generating — and walk each one on a throttled connection, checking four things: is there a submitting state, can it double-submit, is there a more specific message past five seconds, and can a failure be retried or deferred. A browser's throttling mode simulates slow networks fine; no subway trip required.
The standard to hold is this: good loading design makes users barely notice the waiting. Not because waiting vanished, but because every step is accounted for and every failure has a way back. Treat waiting as a first-class feature, and your product gains a feeling of steadiness — and steadiness is exactly why users stay, and why they recommend you to friends.
Comments (0)
Related articles

Errors and empty states are where a product's character shows: say whether it worked, say why, say what is next, and protect the user's work. This guide covers error copy rules, three kinds of empty states, error placement, retry logic, and a pre-launch checklist.

When a new user leaves three minutes after signup, registration is rarely the problem; the first task after it failed. This guide gives vibe-coded projects a first-success method: define one specific, observable first success, pick among sample data, a real task, or import, then handle interruptions, failures, and the single next step.

When a user hits zero results and leaves, the item often exists; they just cannot tell which condition to change. This guide gives vibe-coded projects a search-and-filter method: keyword-filter division of labor, OR-within-group and AND-across-groups logic, counts on options, shareable URL state, and a four-step empty-result recovery.