Загрузка и пустые состояния: скелеты, ожидание и пустые экраны

Что показывать по времени ожидания, скелет вместо крутилки, место заранее, четыре вида пустых экранов и поиск, из которого есть выход, — с живыми примерами.

Интерфейс-элементы Обновлено

Коротко

Пока что-то загружается, интерфейс показывает, что работа идёт и где появится результат; когда показать нечего — объясняет почему и что делать. Что показывать, зависит от ожидания: до секунды — ничего, чтобы индикатор не мелькал; до десяти секунд — скелет в форме будущего содержимого прямо на его месте; дольше — прогресс с шагами и возможность заняться другим. Место под картинки и блоки занимают заранее, чтобы текст не прыгал. Пустой экран никогда не бывает пустым: при первом визите он говорит, что здесь будет, и предлагает одно действие, пустой поиск показывает запрос, убирает лишний фильтр и предлагает близкие запросы, а ошибка загрузки говорит, что случилось, без кодов и предлагает «Повторить».

Что показывать, пока ждут

Классические пороги отклика — 0,1, 1 и 10 секунд — подсказывают, что человек чувствует и что должен показать интерфейс.

Скелет вместо крутилки

Переключите состояния. Крутилка говорит «ждите» посреди пустоты; скелет уже показывает, что появится и где.

Скелет повторяет настоящую вёрстку: те же строки, тот же размер картинок, та же ширина текста. Когда данные приходят, ничего не сдвигается — серые блоки просто заполняются. Крутилку оставляют для коротких действий и для содержимого, форма которого заранее неизвестна.

Место — заранее

Картинка без размеров приходит позже и толкает текст вниз — ровно тогда, когда человек начал читать или собрался нажать.

Без размеров: текст прыгает
Место занято заранее

Достаточно ширины и высоты в разметке: браузер посчитает пропорцию и займёт место до того, как придёт файл. То же — для баннеров, карт и встроенных видео: блок нужного размера стоит на месте с первой секунды. Эти прыжки измеряют и поисковики — в Core Web Vitals.

Что говорит пустое состояние

Пустой экран — начало нового пути или конец законченного. У каждого вида свои слова и своё действие.

Почему пустоЧто сказатьДействие
Первый визит что здесь будет и чем это полезно создать первое
Ничего не нашлось что искали и что есть похожего изменить запрос, убрать фильтр
Всё сделано что работа закончена никакого или следующий полезный шаг
Ошибка загрузки что не получилось, без кодов, и что данные целы повторить
Нет доступа кто это видит и кто может дать доступ попросить доступ

Четыре правильных пустых экрана

Значок, строка объяснения и одно действие. Законченной работе кнопка не нужна — достаточно спокойного слова.

Загрузка в коде: 3 примера

Занятая область со скелетом и картинка, которая держит место, стили скелета и индикатор, который не мелькает на быстрых ответах.

Разметка

aria-busy сообщает читалке, что область обновляется; строка статуса зачитывается; у картинки есть размеры.

orders.html
<!-- Пока грузится: область занята, скелет повторяет список, статус зачитывается -->
<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">

Стили скелета

Скелет виден, пока область занята; перелив выключается для тех, кто просил меньше движения.

skeleton.css
/* Пока область занята — скелет; после — список */
.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;
}

Индикатор, который не мелькает

При первой загрузке скелет виден сразу; при повторных — например после фильтра — только если ответ дольше полсекунды.

orders.js
// Область становится занятой, только если ответ дольше задержки
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 правил загрузки и пустых состояний

  1. 01

    Не мигать

    Индикатор появляется, только если ответ задерживается.

  2. 02

    Загрузка — на месте результата

    Скелет повторяет форму будущего содержимого.

  3. 03

    Место — заранее

    Ширина и высота у картинок, заданный размер у баннеров и карт.

  4. 04

    Долго — показать прогресс

    Шаги, оценка времени и возможность уйти и вернуться.

  5. 05

    Пустота объясняет

    Что здесь будет и одно действие для начала.

  6. 06

    Ничего не нашлось — выход

    Запрос, фильтр, который можно убрать, и близкие запросы.

  7. 07

    Ошибка с «Повторить»

    Слова вместо кода и напоминание, что данные целы.

Частые ошибки загрузки и пустых состояний

  1. Крутилка посреди пустой страницы

    Непонятно, что грузится и сколько ждать.

  2. Скелет другой формы

    Когда приходит содержимое, всё всё равно прыгает.

  3. Текст прыгает из-за картинок

    Человек теряет строку или нажимает не ту кнопку.

  4. «Нет данных» в пустой таблице

    Ни почему пусто, ни что делать.

  5. «Ничего не найдено» — и тупик

    Люди уходят вместо того, чтобы убрать один фильтр.

  6. Вечная крутилка после ошибки

    Запрос давно упал, а страница всё ещё «загружается».

Вопросы о загрузке и пустых состояниях

Скелет или крутилка?

Скелет — для содержимого с известной вёрсткой: списков, карточек, статей. Крутилка — для коротких действий, например у кнопки, и для содержимого неизвестной формы.

Когда показывать индикатор загрузки?

Не сразу: если ответ приходит быстрее полсекунды, индикатор только мелькнёт. Его включают с задержкой; при первой загрузке страницы скелет виден сразу.

Что должно быть в пустом состоянии?

Почему пусто, что здесь будет и одно действие. Иногда простой рисунок, но не вместо слов.

Должен ли скелет переливаться?

Мягкий перелив показывает, что работа идёт. Для тех, кто попросил систему о меньшем движении, он стоит на месте.

Что такое оптимистичный интерфейс?

Результат показывают сразу — лайк, перенесённую карточку, — а запрос уходит в фоне; если он не прошёл, изменение откатывают с сообщением. Подходит для частых мелких действий.

Как экранная читалка узнаёт о загрузке?

Через aria-busy на обновляемой области и короткую строку в role="status": «Загружаем заказы…».

Форма

Без пустых
экранов

Проектирую продукты, где продуманы и ожидание, и пустота — скелеты, пустые состояния и ошибки, которые куда-то ведут. Расскажите о проекте — отвечу в течение рабочего дня.

Или пишите на [email protected]