Скелетон загрузки (Skeleton Screen)

Зачем нужно

Skeleton screen -- это паттерн загрузки, при котором вместо спиннера показывается "скелет" будущего контента: серые блоки той же формы, что и реальные элементы. Это создаёт ощущение быстрой загрузки, потому что пользователь видит структуру страницы ещё до появления данных. Исследования показывают, что скелетоны воспринимаются на 10-20% быстрее, чем спиннеры.

Где используется

  • Ленты новостей и постов (Facebook, LinkedIn)
  • Карточки товаров в каталоге
  • Профили пользователей
  • Списки с данными из API
  • Любой контент, загружаемый асинхронно

Базовый скелетон

HTML

<!-- Скелетон карточки -->
<div class="skeleton-card">
  <div class="skeleton skeleton--image"></div>
  <div class="skeleton skeleton--title"></div>
  <div class="skeleton skeleton--text"></div>
  <div class="skeleton skeleton--text skeleton--short"></div>
</div>

<!-- Реальная карточка (скрыта до загрузки) -->
<article class="card" hidden>
  <img class="card__image" src="" alt="" />
  <h3 class="card__title"></h3>
  <p class="card__text"></p>
</article>

CSS с анимацией shimmer

.skeleton {
  background: #e0e0e0;
  border-radius: 4px;
  position: relative;
  overflow: hidden;
}

/* Shimmer-эффект (волна света) */
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(255, 255, 255, 0.4) 50%,
    transparent 100%
  );
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(100%);
  }
}

/* Формы скелетон-элементов */
.skeleton--image {
  width: 100%;
  height: 200px;
  border-radius: 8px;
}

.skeleton--title {
  width: 70%;
  height: 24px;
  margin-top: 16px;
}

.skeleton--text {
  width: 100%;
  height: 16px;
  margin-top: 12px;
}

.skeleton--short {
  width: 50%;
}

.skeleton--avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
}

.skeleton--button {
  width: 120px;
  height: 36px;
  border-radius: 8px;
}

Скелетон для карточки с аватаром

<div class="skeleton-profile">
  <div class="skeleton-profile__header">
    <div class="skeleton skeleton--avatar"></div>
    <div class="skeleton-profile__info">
      <div class="skeleton skeleton--title" style="width: 40%"></div>
      <div class="skeleton skeleton--text" style="width: 60%"></div>
    </div>
  </div>
  <div class="skeleton skeleton--image" style="height: 300px"></div>
  <div class="skeleton skeleton--text"></div>
  <div class="skeleton skeleton--text skeleton--short"></div>
</div>
.skeleton-profile__header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

.skeleton-profile__info {
  flex: 1;
}

Переход от скелетона к реальному контенту

JavaScript

async function loadCards() {
  const container = document.getElementById('cardContainer');

  // 1. Показать скелетоны
  container.innerHTML = renderSkeletons(6);

  try {
    // 2. Загрузить данные
    const response = await fetch('/api/cards');
    const cards = await response.json();

    // 3. Заменить скелетоны на реальный контент с анимацией
    container.innerHTML = cards.map(renderCard).join('');
    container.querySelectorAll('.card').forEach((card, i) => {
      card.style.animationDelay = `${i * 0.05}s`;
      card.classList.add('card--fade-in');
    });
  } catch (error) {
    container.innerHTML = '<p class="error">Ошибка загрузки</p>';
  }
}

function renderSkeletons(count) {
  return Array.from({ length: count }, () => `
    <div class="skeleton-card">
      <div class="skeleton skeleton--image"></div>
      <div class="skeleton skeleton--title"></div>
      <div class="skeleton skeleton--text"></div>
      <div class="skeleton skeleton--text skeleton--short"></div>
    </div>
  `).join('');
}

function renderCard(data) {
  return `
    <article class="card">
      <img class="card__image" src="${data.image}" alt="${data.title}" />
      <h3 class="card__title">${data.title}</h3>
      <p class="card__text">${data.description}</p>
    </article>
  `;
}

CSS для плавного появления

.card--fade-in {
  animation: fadeInUp 0.3s ease-out forwards;
  opacity: 0;
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

Pulse-вариант (альтернатива shimmer)

.skeleton--pulse {
  animation: pulse 1.5s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
}

Prefers-reduced-motion

@media (prefers-reduced-motion: reduce) {
  .skeleton::after {
    animation: none;
  }

  .skeleton--pulse {
    animation: none;
  }
}

Частые ошибки

Ошибка Проблема Решение
Скелетон не совпадает с реальным layout Сдвиг при появлении контента Повтори размеры реальных элементов
Бесконечный shimmer без timeout Если API упал, скелетон навечно Добавь таймаут и fallback
Нет prefers-reduced-motion Мерцание для чувствительных Отключай анимацию
Слишком много скелетонов Перегружает экран Покажи 3-6 placeholder
Резкий переход skeleton -> контент Визуальный "скачок" Анимация fadeIn при появлении

Связанные темы

Ресурсы