← HTML & CSS/Переходы и анимации CSS#30 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: старт в frontendПрактика: DOM и событияТермин: DOMМаршрут: старт с нуля

Переходы и анимации CSS

Кнопка плавно меняет цвет при наведении. Карточка немного увеличивается при ховере. Спиннер крутится пока загружается данные. Уведомление выезжает снизу и плавно исчезает. Всё это — CSS-анимации и переходы. Без них интерфейс ощущается дёрганым и устаревшим.

transition — плавный переход

transition делает изменение CSS-свойства плавным.

.btn {
  background: #7b2ff7;
  transition: background 0.2s ease;
}
.btn:hover {
  background: #6b21d4;
}

Синтаксис

transition: свойство длительность timing-function задержка;

/* Примеры */
transition: background 0.3s ease;
transition: all 0.2s ease-in-out;
transition: background 0.2s, transform 0.1s;   /* Несколько свойств */

Timing functions

transition: ... ease;         /* Медленно → быстро → медленно (по умолчанию) */
transition: ... ease-in;      /* Медленно → быстро */
transition: ... ease-out;     /* Быстро → медленно */
transition: ... ease-in-out;  /* Медленно → быстро → медленно (симметрично) */
transition: ... linear;       /* Равномерно */
transition: ... cubic-bezier(0.4, 0, 0.2, 1);  /* Кастомная кривая (Material Design) */

transform — трансформация элемента

transform изменяет форму, положение и размер элемента не влияя на поток документа.

/* Сдвиг */
transform: translateX(20px);
transform: translateY(-10px);
transform: translate(20px, -10px);  /* x и y */
transform: translateX(50%);          /* 50% от ширины самого элемента */

/* Масштаб */
transform: scale(1.05);    /* Увеличить на 5% */
transform: scale(0.95);    /* Уменьшить на 5% */
transform: scaleX(1.2);    /* Только по X */

/* Поворот */
transform: rotate(45deg);   /* По часовой на 45° */
transform: rotate(-90deg);  /* Против часовой на 90° */

/* Наклон */
transform: skewX(10deg);

/* Несколько трансформаций (порядок важен!) */
transform: translateX(-50%) rotate(45deg);

Центрирование через translate

.modal {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);  /* Сдвинуть на -50% своей ширины и высоты */
}

@keyframes — сложные анимации

Для более сложных анимаций — с несколькими этапами — используй @keyframes.

@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

@keyframes fadeIn {
  0%   { opacity: 0; transform: translateY(20px); }
  100% { opacity: 1; transform: translateY(0); }
}

@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50%       { transform: scale(1.05); }
}

@keyframes shimmer {
  0%   { background-position: -1000px 0; }
  100% { background-position: 1000px 0; }
}

animation — применение анимации

animation: имя длительность timing-function задержка количество направление fill-mode;

.spinner {
  animation: spin 1s linear infinite;
}

.card-appear {
  animation: fadeIn 0.3s ease-out;
}

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

Ключевые свойства animation

animation-name: fadeIn;            /* Имя @keyframes */
animation-duration: 0.3s;          /* Длительность */
animation-timing-function: ease;   /* Кривая */
animation-delay: 0.1s;             /* Задержка перед стартом */
animation-iteration-count: 1;      /* Количество повторений (infinite — бесконечно) */
animation-direction: normal;       /* normal | reverse | alternate | alternate-reverse */
animation-fill-mode: forwards;     /* Что делать после окончания */
animation-play-state: running;     /* running | paused */

will-change и производительность

Что нужно знать

Браузер выполняет CSS быстрее, если анимируются только transform и opacity. Эти свойства обрабатываются на GPU.

/* Медленно — вызывает перерисовку (layout/paint) */
transition: width 0.3s;
transition: top 0.3s;
transition: margin 0.3s;

/* Быстро — только compositing (GPU) */
transition: transform 0.3s;
transition: opacity 0.3s;
/* Подсказка браузеру заранее подготовить слой */
.animated-element {
  will-change: transform, opacity;
}

Не злоупотребляй will-change — каждый такой элемент создаёт отдельный GPU-слой, что увеличивает потребление памяти.

Типичные ошибки

Ошибка 1: Анимировать width/height вместо transform

/* Плохо — перерисовывает layout */
.card:hover { width: 110%; }
/* Хорошо — только compositing */
.card:hover { transform: scale(1.05); }

Ошибка 2: transition: all — плохая практика

transition: all 0.3s;  /* Анимирует всё — включая display, что невозможно анимировать */
transition: background 0.3s, transform 0.2s;  /* Явно указывай свойства */

Ошибка 3: Слишком долгие анимации

transition: all 1s;  /* 1 секунда — вечность для пользователя */
transition: all 0.2s;  /* 200ms — воспринимается как мгновенное */

В реальных проектах

Каждая кнопка на Wildberries, каждая карточка на Avito, каждое уведомление в Telegram Web — всё это CSS-анимации. Принцип: анимации должны ощущаться мгновенными (100-300ms) или намеренно медленными (скелетон-лоадер, 1-2s). Всё между — раздражает.

Framer Motion и GSAP используются для сложных React-анимаций, но под капотом те же CSS transform и opacity.

Примеры

Переходы, трансформации и keyframe-анимации в действии

const style = document.createElement('style')
style.textContent = `
  * { box-sizing: border-box; }
  body { font-family: Arial, sans-serif; padding: 24px; background: #f7fafc; }

  /* Кнопка с transition */
  .btn {
    background: #7b2ff7;
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 8px;
    font-size: 14px;
    cursor: pointer;
    transition: background 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
    margin: 8px;
  }
  .btn:hover {
    background: #6b21d4;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(123,47,247,0.4);
  }
  .btn:active {
    transform: translateY(0) scale(0.97);
    box-shadow: none;
  }

  /* Спиннер через @keyframes */
  @keyframes spin {
    to { transform: rotate(360deg); }
  }
  .spinner {
    width: 32px;
    height: 32px;
    border: 3px solid #e2e8f0;
    border-top-color: #7b2ff7;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    margin: 16px;
  }

  /* Пульсирующий скелетон */
  @keyframes shimmer {
    0% { background-position: -400px 0; }
    100% { background-position: 400px 0; }
  }
  .skeleton {
    height: 16px;
    border-radius: 4px;
    background: linear-gradient(90deg, #e2e8f0 25%, #f7fafc 50%, #e2e8f0 75%);
    background-size: 800px 100%;
    animation: shimmer 1.5s infinite;
    margin: 8px 0;
  }

  /* Появление через fadeIn */
  @keyframes fadeInUp {
    from { opacity: 0; transform: translateY(16px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .fade-card {
    background: white;
    border-radius: 12px;
    padding: 16px;
    box-shadow: 0 1px 4px rgba(0,0,0,0.1);
    margin: 8px 0;
    animation: fadeInUp 0.4s ease-out forwards;
  }

  /* Карточка с hover transform */
  .product-card {
    display: inline-block;
    background: white;
    border-radius: 12px;
    padding: 16px;
    margin: 8px;
    cursor: pointer;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
  }
  .product-card:hover {
    transform: translateY(-4px) scale(1.02);
    box-shadow: 0 8px 24px rgba(0,0,0,0.12);
  }
`
document.head.appendChild(style)

// Кнопки
const btn1 = document.createElement('button')
btn1.className = 'btn'
btn1.textContent = 'Наведи на меня'
document.body.appendChild(btn1)

// Спиннер загрузки
const spinnerLabel = document.createElement('p')
spinnerLabel.textContent = 'Спиннер загрузки:'
spinnerLabel.style.margin = '16px 0 0'
document.body.appendChild(spinnerLabel)
const spinner = document.createElement('div')
spinner.className = 'spinner'
document.body.appendChild(spinner)

// Скелетон
const skeletonLabel = document.createElement('p')
skeletonLabel.textContent = 'Скелетон-лоадер:'
document.body.appendChild(skeletonLabel)
;[200, 160, 120].forEach(width => {
  const sk = document.createElement('div')
  sk.className = 'skeleton'
  sk.style.width = width + 'px'
  document.body.appendChild(sk)
})

// Карточки с анимацией появления
const cardsLabel = document.createElement('p')
cardsLabel.textContent = 'Карточки с fadeInUp:'
document.body.appendChild(cardsLabel)
;['Урок 1: Введение', 'Урок 2: Переменные', 'Урок 3: Типы'].forEach((text, i) => {
  const card = document.createElement('div')
  card.className = 'fade-card'
  card.style.animationDelay = (i * 0.1) + 's'  // Каскадное появление
  card.textContent = text
  document.body.appendChild(card)
})

// Карточка товара
const productCard = document.createElement('div')
productCard.className = 'product-card'
productCard.innerHTML = '<strong>Nike Air</strong><br>4 990 ₽'
document.body.appendChild(productCard)

// Читаем стили анимации
const spinnerStyle = window.getComputedStyle(spinner)
console.log('Spinner animation-name:', spinnerStyle.animationName)        // spin
console.log('Spinner animation-duration:', spinnerStyle.animationDuration) // 0.8s
console.log('Spinner animation-iteration:', spinnerStyle.animationIterationCount) // infinite

const btnStyle = window.getComputedStyle(btn1)
console.log('Btn transition:', btnStyle.transition)

Переходы и анимации CSS

Кнопка плавно меняет цвет при наведении. Карточка немного увеличивается при ховере. Спиннер крутится пока загружается данные. Уведомление выезжает снизу и плавно исчезает. Всё это — CSS-анимации и переходы. Без них интерфейс ощущается дёрганым и устаревшим.

transition — плавный переход

transition делает изменение CSS-свойства плавным.

.btn {
  background: #7b2ff7;
  transition: background 0.2s ease;
}
.btn:hover {
  background: #6b21d4;
}

Синтаксис

transition: свойство длительность timing-function задержка;

/* Примеры */
transition: background 0.3s ease;
transition: all 0.2s ease-in-out;
transition: background 0.2s, transform 0.1s;   /* Несколько свойств */

Timing functions

transition: ... ease;         /* Медленно → быстро → медленно (по умолчанию) */
transition: ... ease-in;      /* Медленно → быстро */
transition: ... ease-out;     /* Быстро → медленно */
transition: ... ease-in-out;  /* Медленно → быстро → медленно (симметрично) */
transition: ... linear;       /* Равномерно */
transition: ... cubic-bezier(0.4, 0, 0.2, 1);  /* Кастомная кривая (Material Design) */

transform — трансформация элемента

transform изменяет форму, положение и размер элемента не влияя на поток документа.

/* Сдвиг */
transform: translateX(20px);
transform: translateY(-10px);
transform: translate(20px, -10px);  /* x и y */
transform: translateX(50%);          /* 50% от ширины самого элемента */

/* Масштаб */
transform: scale(1.05);    /* Увеличить на 5% */
transform: scale(0.95);    /* Уменьшить на 5% */
transform: scaleX(1.2);    /* Только по X */

/* Поворот */
transform: rotate(45deg);   /* По часовой на 45° */
transform: rotate(-90deg);  /* Против часовой на 90° */

/* Наклон */
transform: skewX(10deg);

/* Несколько трансформаций (порядок важен!) */
transform: translateX(-50%) rotate(45deg);

Центрирование через translate

.modal {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);  /* Сдвинуть на -50% своей ширины и высоты */
}

@keyframes — сложные анимации

Для более сложных анимаций — с несколькими этапами — используй @keyframes.

@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

@keyframes fadeIn {
  0%   { opacity: 0; transform: translateY(20px); }
  100% { opacity: 1; transform: translateY(0); }
}

@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50%       { transform: scale(1.05); }
}

@keyframes shimmer {
  0%   { background-position: -1000px 0; }
  100% { background-position: 1000px 0; }
}

animation — применение анимации

animation: имя длительность timing-function задержка количество направление fill-mode;

.spinner {
  animation: spin 1s linear infinite;
}

.card-appear {
  animation: fadeIn 0.3s ease-out;
}

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

Ключевые свойства animation

animation-name: fadeIn;            /* Имя @keyframes */
animation-duration: 0.3s;          /* Длительность */
animation-timing-function: ease;   /* Кривая */
animation-delay: 0.1s;             /* Задержка перед стартом */
animation-iteration-count: 1;      /* Количество повторений (infinite — бесконечно) */
animation-direction: normal;       /* normal | reverse | alternate | alternate-reverse */
animation-fill-mode: forwards;     /* Что делать после окончания */
animation-play-state: running;     /* running | paused */

will-change и производительность

Что нужно знать

Браузер выполняет CSS быстрее, если анимируются только transform и opacity. Эти свойства обрабатываются на GPU.

/* Медленно — вызывает перерисовку (layout/paint) */
transition: width 0.3s;
transition: top 0.3s;
transition: margin 0.3s;

/* Быстро — только compositing (GPU) */
transition: transform 0.3s;
transition: opacity 0.3s;
/* Подсказка браузеру заранее подготовить слой */
.animated-element {
  will-change: transform, opacity;
}

Не злоупотребляй will-change — каждый такой элемент создаёт отдельный GPU-слой, что увеличивает потребление памяти.

Типичные ошибки

Ошибка 1: Анимировать width/height вместо transform

/* Плохо — перерисовывает layout */
.card:hover { width: 110%; }
/* Хорошо — только compositing */
.card:hover { transform: scale(1.05); }

Ошибка 2: transition: all — плохая практика

transition: all 0.3s;  /* Анимирует всё — включая display, что невозможно анимировать */
transition: background 0.3s, transform 0.2s;  /* Явно указывай свойства */

Ошибка 3: Слишком долгие анимации

transition: all 1s;  /* 1 секунда — вечность для пользователя */
transition: all 0.2s;  /* 200ms — воспринимается как мгновенное */

В реальных проектах

Каждая кнопка на Wildberries, каждая карточка на Avito, каждое уведомление в Telegram Web — всё это CSS-анимации. Принцип: анимации должны ощущаться мгновенными (100-300ms) или намеренно медленными (скелетон-лоадер, 1-2s). Всё между — раздражает.

Framer Motion и GSAP используются для сложных React-анимаций, но под капотом те же CSS transform и opacity.

Примеры

Переходы, трансформации и keyframe-анимации в действии

const style = document.createElement('style')
style.textContent = `
  * { box-sizing: border-box; }
  body { font-family: Arial, sans-serif; padding: 24px; background: #f7fafc; }

  /* Кнопка с transition */
  .btn {
    background: #7b2ff7;
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 8px;
    font-size: 14px;
    cursor: pointer;
    transition: background 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
    margin: 8px;
  }
  .btn:hover {
    background: #6b21d4;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(123,47,247,0.4);
  }
  .btn:active {
    transform: translateY(0) scale(0.97);
    box-shadow: none;
  }

  /* Спиннер через @keyframes */
  @keyframes spin {
    to { transform: rotate(360deg); }
  }
  .spinner {
    width: 32px;
    height: 32px;
    border: 3px solid #e2e8f0;
    border-top-color: #7b2ff7;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    margin: 16px;
  }

  /* Пульсирующий скелетон */
  @keyframes shimmer {
    0% { background-position: -400px 0; }
    100% { background-position: 400px 0; }
  }
  .skeleton {
    height: 16px;
    border-radius: 4px;
    background: linear-gradient(90deg, #e2e8f0 25%, #f7fafc 50%, #e2e8f0 75%);
    background-size: 800px 100%;
    animation: shimmer 1.5s infinite;
    margin: 8px 0;
  }

  /* Появление через fadeIn */
  @keyframes fadeInUp {
    from { opacity: 0; transform: translateY(16px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .fade-card {
    background: white;
    border-radius: 12px;
    padding: 16px;
    box-shadow: 0 1px 4px rgba(0,0,0,0.1);
    margin: 8px 0;
    animation: fadeInUp 0.4s ease-out forwards;
  }

  /* Карточка с hover transform */
  .product-card {
    display: inline-block;
    background: white;
    border-radius: 12px;
    padding: 16px;
    margin: 8px;
    cursor: pointer;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
  }
  .product-card:hover {
    transform: translateY(-4px) scale(1.02);
    box-shadow: 0 8px 24px rgba(0,0,0,0.12);
  }
`
document.head.appendChild(style)

// Кнопки
const btn1 = document.createElement('button')
btn1.className = 'btn'
btn1.textContent = 'Наведи на меня'
document.body.appendChild(btn1)

// Спиннер загрузки
const spinnerLabel = document.createElement('p')
spinnerLabel.textContent = 'Спиннер загрузки:'
spinnerLabel.style.margin = '16px 0 0'
document.body.appendChild(spinnerLabel)
const spinner = document.createElement('div')
spinner.className = 'spinner'
document.body.appendChild(spinner)

// Скелетон
const skeletonLabel = document.createElement('p')
skeletonLabel.textContent = 'Скелетон-лоадер:'
document.body.appendChild(skeletonLabel)
;[200, 160, 120].forEach(width => {
  const sk = document.createElement('div')
  sk.className = 'skeleton'
  sk.style.width = width + 'px'
  document.body.appendChild(sk)
})

// Карточки с анимацией появления
const cardsLabel = document.createElement('p')
cardsLabel.textContent = 'Карточки с fadeInUp:'
document.body.appendChild(cardsLabel)
;['Урок 1: Введение', 'Урок 2: Переменные', 'Урок 3: Типы'].forEach((text, i) => {
  const card = document.createElement('div')
  card.className = 'fade-card'
  card.style.animationDelay = (i * 0.1) + 's'  // Каскадное появление
  card.textContent = text
  document.body.appendChild(card)
})

// Карточка товара
const productCard = document.createElement('div')
productCard.className = 'product-card'
productCard.innerHTML = '<strong>Nike Air</strong><br>4 990 ₽'
document.body.appendChild(productCard)

// Читаем стили анимации
const spinnerStyle = window.getComputedStyle(spinner)
console.log('Spinner animation-name:', spinnerStyle.animationName)        // spin
console.log('Spinner animation-duration:', spinnerStyle.animationDuration) // 0.8s
console.log('Spinner animation-iteration:', spinnerStyle.animationIterationCount) // infinite

const btnStyle = window.getComputedStyle(btn1)
console.log('Btn transition:', btnStyle.transition)

Задание

Создай анимацию прыгающего мяча с помощью `@keyframes bounce`. Мяч должен подниматься на 20px вверх (translateY(-20px)) в середине анимации и возвращаться обратно. Добавь спиннер загрузки, который крутится бесконечно через `@keyframes spin`.

Подсказка

Для `bounce`: 0% и 100% — `translateY(0)`, 50% — `translateY(-20px)`. Длительность анимации `0.6s`. Для `spin`: `from` — `rotate(0deg)`, `to` — `rotate(360deg)`, длительность `1s`.

Загружаем среду выполнения...
Загружаем AI-помощника...