Loading and empty states: skeletons, waiting and blank screens
What to show by the length of the wait, a skeleton instead of a spinner, space reserved in advance, four kinds of empty screens and a search with a way out — with live examples.
In short
While something loads, the interface shows that work is going on and where the result will appear; when there is nothing to show, it explains why and what to do. What to show depends on the wait: up to a second — nothing, so the indicator does not flash; up to ten seconds — a skeleton in the shape of the future content right where it will be; longer — progress with steps and the option to do something else. Space for images and blocks is reserved in advance, so the text does not jump. An empty screen is never blank: the first visit says what will be here and offers one action, an empty search shows the query, removes the extra filter and suggests close queries, and a loading error says what happened without codes and offers “Retry”.
What to show while people wait
Classic response time limits — 0.1, 1 and 10 seconds — tell what a person feels and what the interface should show.
A skeleton instead of a spinner
Switch the states. The spinner says “wait” in an empty box; the skeleton already shows what will appear and where.
A skeleton repeats the real layout: the same rows, the same image size, the same text width. When the data arrives, nothing moves — the grey blocks are simply filled in. A spinner is left for short actions and for content whose shape is not known in advance.
Reserve the space in advance
An image without a size arrives late and pushes the text down — right when the person started reading or was about to press.
Width and height in the markup are enough: the browser calculates the proportion and keeps the space before the file arrives. The same goes for banners, maps and embedded videos — a block of the right size is in place from the first moment. Search engines measure these jumps too, as part of Core Web Vitals.
What an empty state says
An empty screen is the start of a new path or the end of a finished one. Each kind has its own words and its own action.
| Why it is empty | What to say | Action |
|---|---|---|
| First visit | what will be here and why it is useful | create the first item |
| Nothing found | what was searched and what is close | change the query, remove a filter |
| Everything is done | that the work is finished | none, or the next useful step |
| Loading error | what failed, without codes, and that the data is safe | retry |
| No access | who sees this and who can give access | ask for access |
Four empty screens done right
A sign, one line of explanation and one action. A finished task needs no button — just a calm word.
Nothing found — but there is a way out
Most empty searches are caused by a filter or one extra word. The page can see that and offer the way out itself.
Loading in code: 3 examples
A busy region with a skeleton and an image that keeps its space, skeleton styles, and a loader that does not flash on fast answers.
Markup
aria-busy tells screen readers the region is updating; the status line is read aloud; the image has its size.
<!-- While loading: the region is busy, the skeleton repeats the list, the status is read aloud -->
<section class="orders" aria-labelledby="orders-title" aria-busy="true">
<h2 id="orders-title">Orders</h2>
<p class="visually-hidden" role="status">Loading orders…</p>
<ul class="skeleton" aria-hidden="true">
<li class="skeleton__row"></li>
<li class="skeleton__row"></li>
<li class="skeleton__row"></li>
</ul>
<ul class="orders__list"></ul>
</section>
<!-- An image with its size: the browser keeps the space before the file arrives -->
<img src="/img/kitchen.webp" width="1200" height="900"
alt="A kitchen with an oak worktop" loading="lazy">
Skeleton styles
The skeleton is shown while the region is busy; the shimmer stops for those who asked for less motion.
/* While the region is busy — the skeleton; after — the list */
.orders[aria-busy="true"] .orders__list,
.orders:not([aria-busy="true"]) .skeleton {
display: none;
}
.skeleton__row {
height: 4rem;
border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--color-skeleton), var(--color-skeleton-light), var(--color-skeleton));
background-size: 200% 100%;
animation: shimmer 1.6s linear infinite;
}
@keyframes shimmer {
to { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
.skeleton__row { animation: none; }
}
/* With width and height in the markup, this keeps the proportion */
img {
max-width: 100%;
height: auto;
}
A loader that does not flash
The first load shows the skeleton at once; repeated loads — for example after a filter — only if the answer takes longer than half a second.
// The region becomes busy only if the answer takes longer than the delay
async function withLoader(region, task, delay = 500) {
const timer = setTimeout(() => region.setAttribute('aria-busy', 'true'), delay);
try {
return await task();
} finally {
clearTimeout(timer);
region.setAttribute('aria-busy', 'false');
}
}
const section = document.querySelector('.orders');
const filters = document.querySelector('.filters');
filters.addEventListener('change', async () => {
const query = new URLSearchParams(new FormData(filters));
const orders = await withLoader(section, () =>
fetch(`/api/orders?${query}`).then((response) => response.json()));
renderOrders(orders);
});
7 rules for loading and empty states
-
01
Do not flash
The indicator appears only if the answer is late.
-
02
Load in place
The skeleton has the shape of the future content.
-
03
Reserve the space
Width and height for images, a fixed size for banners and maps.
-
04
Long — show progress
Steps, a time estimate and the option to leave and come back.
-
05
Emptiness explains
What will be here and one action to begin with.
-
06
No results — a way out
The query, the filter to remove and close queries.
-
07
An error with “Retry”
Words instead of a code and a reminder that the data is safe.
Common mistakes with loading and empty states
-
A spinner in the middle of a blank page
It is not clear what is loading or how long to wait.
-
A skeleton of a different shape
When the content arrives, everything jumps anyway.
-
Text that jumps under images
The person loses the line or presses the wrong button.
-
“No data” in an empty table
Neither why it is empty nor what to do.
-
“Nothing found” and a dead end
People leave instead of removing one filter.
-
An endless spinner after an error
The request failed long ago, and the page keeps “loading”.
Questions about loading and empty states
Skeleton or spinner?
A skeleton for content with a known layout: lists, cards, articles. A spinner for short actions, such as a button, and for content whose shape is unknown.
When should a loading indicator appear?
Not at once: if the answer comes within half a second, the indicator would only flash. It is switched on with a delay; the first load of a page shows the skeleton immediately.
What should an empty state contain?
Why it is empty, what will be here and one action. Sometimes a simple drawing, but not instead of the words.
Should a skeleton shimmer?
A gentle shimmer shows that work is going on. For those who asked the system for less motion it stays still.
What is an optimistic interface?
The result is shown at once — a like, a moved card — and the request goes in the background; if it fails, the change is rolled back with a message. It suits frequent small actions.
How does a screen reader know that something is loading?
aria-busy on the region that is updating and a short line in role="status": “Loading orders…”.
Online form
No blank
screens
I design products where waiting and emptiness are planned too — skeletons, empty states and errors that lead somewhere. Tell me about the project — I answer within one working day.