Загрузка и пустые состояния: скелеты, ожидание и пустые экраны
Что показывать по времени ожидания, скелет вместо крутилки, место заранее, четыре вида пустых экранов и поиск, из которого есть выход, — с живыми примерами.
Коротко
Пока что-то загружается, интерфейс показывает, что работа идёт и где появится результат; когда показать нечего — объясняет почему и что делать. Что показывать, зависит от ожидания: до секунды — ничего, чтобы индикатор не мелькал; до десяти секунд — скелет в форме будущего содержимого прямо на его месте; дольше — прогресс с шагами и возможность заняться другим. Место под картинки и блоки занимают заранее, чтобы текст не прыгал. Пустой экран никогда не бывает пустым: при первом визите он говорит, что здесь будет, и предлагает одно действие, пустой поиск показывает запрос, убирает лишний фильтр и предлагает близкие запросы, а ошибка загрузки говорит, что случилось, без кодов и предлагает «Повторить».
Что показывать, пока ждут
Классические пороги отклика — 0,1, 1 и 10 секунд — подсказывают, что человек чувствует и что должен показать интерфейс.
Скелет вместо крутилки
Переключите состояния. Крутилка говорит «ждите» посреди пустоты; скелет уже показывает, что появится и где.
Скелет повторяет настоящую вёрстку: те же строки, тот же размер картинок, та же ширина текста. Когда данные приходят, ничего не сдвигается — серые блоки просто заполняются. Крутилку оставляют для коротких действий и для содержимого, форма которого заранее неизвестна.
Место — заранее
Картинка без размеров приходит позже и толкает текст вниз — ровно тогда, когда человек начал читать или собрался нажать.
Достаточно ширины и высоты в разметке: браузер посчитает пропорцию и займёт место до того, как придёт файл. То же — для баннеров, карт и встроенных видео: блок нужного размера стоит на месте с первой секунды. Эти прыжки измеряют и поисковики — в Core Web Vitals.
Что говорит пустое состояние
Пустой экран — начало нового пути или конец законченного. У каждого вида свои слова и своё действие.
| Почему пусто | Что сказать | Действие |
|---|---|---|
| Первый визит | что здесь будет и чем это полезно | создать первое |
| Ничего не нашлось | что искали и что есть похожего | изменить запрос, убрать фильтр |
| Всё сделано | что работа закончена | никакого или следующий полезный шаг |
| Ошибка загрузки | что не получилось, без кодов, и что данные целы | повторить |
| Нет доступа | кто это видит и кто может дать доступ | попросить доступ |
Четыре правильных пустых экрана
Значок, строка объяснения и одно действие. Законченной работе кнопка не нужна — достаточно спокойного слова.
Ничего не нашлось — но есть выход
Чаще всего поиск пуст из-за фильтра или одного лишнего слова. Страница может это заметить и сама предложить выход.
Загрузка в коде: 3 примера
Занятая область со скелетом и картинка, которая держит место, стили скелета и индикатор, который не мелькает на быстрых ответах.
Разметка
aria-busy сообщает читалке, что область обновляется; строка статуса зачитывается; у картинки есть размеры.
<!-- Пока грузится: область занята, скелет повторяет список, статус зачитывается -->
<section class="orders" aria-labelledby="orders-title" aria-busy="true">
<h2 id="orders-title">Заказы</h2>
<p class="visually-hidden" role="status">Загружаем заказы…</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>
<!-- Картинка с размерами: браузер держит место, пока файл не пришёл -->
<img src="/img/kitchen.webp" width="1200" height="900"
alt="Кухня с дубовой столешницей" loading="lazy">
Стили скелета
Скелет виден, пока область занята; перелив выключается для тех, кто просил меньше движения.
/* Пока область занята — скелет; после — список */
.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; }
}
/* С шириной и высотой в разметке это сохраняет пропорцию */
img {
max-width: 100%;
height: auto;
}
Индикатор, который не мелькает
При первой загрузке скелет виден сразу; при повторных — например после фильтра — только если ответ дольше полсекунды.
// Область становится занятой, только если ответ дольше задержки
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 правил загрузки и пустых состояний
-
01
Не мигать
Индикатор появляется, только если ответ задерживается.
-
02
Загрузка — на месте результата
Скелет повторяет форму будущего содержимого.
-
03
Место — заранее
Ширина и высота у картинок, заданный размер у баннеров и карт.
-
04
Долго — показать прогресс
Шаги, оценка времени и возможность уйти и вернуться.
-
05
Пустота объясняет
Что здесь будет и одно действие для начала.
-
06
Ничего не нашлось — выход
Запрос, фильтр, который можно убрать, и близкие запросы.
-
07
Ошибка с «Повторить»
Слова вместо кода и напоминание, что данные целы.
Частые ошибки загрузки и пустых состояний
-
Крутилка посреди пустой страницы
Непонятно, что грузится и сколько ждать.
-
Скелет другой формы
Когда приходит содержимое, всё всё равно прыгает.
-
Текст прыгает из-за картинок
Человек теряет строку или нажимает не ту кнопку.
-
«Нет данных» в пустой таблице
Ни почему пусто, ни что делать.
-
«Ничего не найдено» — и тупик
Люди уходят вместо того, чтобы убрать один фильтр.
-
Вечная крутилка после ошибки
Запрос давно упал, а страница всё ещё «загружается».
Вопросы о загрузке и пустых состояниях
Скелет или крутилка?
Скелет — для содержимого с известной вёрсткой: списков, карточек, статей. Крутилка — для коротких действий, например у кнопки, и для содержимого неизвестной формы.
Когда показывать индикатор загрузки?
Не сразу: если ответ приходит быстрее полсекунды, индикатор только мелькнёт. Его включают с задержкой; при первой загрузке страницы скелет виден сразу.
Что должно быть в пустом состоянии?
Почему пусто, что здесь будет и одно действие. Иногда простой рисунок, но не вместо слов.
Должен ли скелет переливаться?
Мягкий перелив показывает, что работа идёт. Для тех, кто попросил систему о меньшем движении, он стоит на месте.
Что такое оптимистичный интерфейс?
Результат показывают сразу — лайк, перенесённую карточку, — а запрос уходит в фоне; если он не прошёл, изменение откатывают с сообщением. Подходит для частых мелких действий.
Как экранная читалка узнаёт о загрузке?
Через aria-busy на обновляемой области и короткую строку в role="status": «Загружаем заказы…».
Форма
Без пустых
экранов
Проектирую продукты, где продуманы и ожидание, и пустота — скелеты, пустые состояния и ошибки, которые куда-то ведут. Расскажите о проекте — отвечу в течение рабочего дня.