Скелетон загрузки (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 при появлении |
Связанные темы
- Ленивая загрузка изображений -- LQIP (low-quality image placeholder)
- Бесконечный скролл -- скелетоны при подгрузке страниц
- Слайдер -- placeholder при загрузке слайдов
- Tooltip -- визуальный отклик