Hover эффекты

Hover-эффекты — это реакция интерфейса на наведение курсора. В библиотеке Motion One они реализуются декларативно и с высокой производительностью за счёт использования Web Animations API. Основная задача — обеспечить плавную, отзывчивую и легко управляемую анимацию при изменении состояния элемента.

Ключевое преимущество Motion One — минималистичный API, позволяющий описывать сложные взаимодействия без перегруженного кода.


Базовая анимация при наведении

Самый простой способ — использовать обработчики событий mouseenter и mouseleave:

import { animate } from "motion"

const button = document.querySelector(".btn")

button.addEventListener("mouseenter", () => {
  animate(button, { scale: 1.1 }, { duration: 0.2 })
})

button.addEventListener("mouseleave", () => {
  animate(button, { scale: 1 }, { duration: 0.2 })
})

Ключевые моменты:

  • animate() автоматически создаёт и управляет анимацией
  • значения интерполируются плавно
  • не требуется ручное управление requestAnimationFrame

Использование hover() — декларативный подход

Motion One предоставляет утилиту hover(), упрощающую работу с hover-состояниями:

import { hover, animate } from "motion"

const button = document.querySelector(".btn")

hover(button, () => {
  return animate(button, { scale: 1.1 })
})

Возвращаемое значение — это функция отмены анимации, которая автоматически вызывается при уходе курсора.


Управление состояниями: вход и выход

Для более сложного поведения можно явно разделить состояния:

hover(button, () => {
  const enterAnimation = animate(button, { scale: 1.1 })

  return () => {
    animate(button, { scale: 1 })
  }
})

Такой подход даёт полный контроль над логикой выхода из hover-состояния.


Анимация нескольких свойств

Hover-эффекты редко ограничиваются масштабированием. Часто комбинируются:

hover(card, () => {
  return animate(card, {
    scale: 1.05,
    rotate: "1deg",
    boxShadow: "0 10px 30px rgba(0,0,0,0.2)"
  }, {
    duration: 0.3,
    easing: "ease-out"
  })
})

Популярные свойства:

  • transform (scale, rotate, translate)
  • opacity
  • filter (blur, brightness)
  • box-shadow

Работа с easing

Плавность анимации определяется функцией easing:

animate(element, { scale: 1.1 }, {
  duration: 0.3,
  easing: [0.25, 0.1, 0.25, 1]
})

Часто используемые варианты:

  • ease-in — ускорение
  • ease-out — замедление
  • ease-in-out — комбинированный вариант

Кастомные cubic-bezier позволяют добиться уникального поведения.


Последовательные hover-анимации

Можно комбинировать несколько анимаций:

hover(button, () => {
  return animate(button, [
    { scale: 1 },
    { scale: 1.15 },
    { scale: 1.1 }
  ], {
    duration: 0.4
  })
})

Это создаёт эффект «переразгона» — сначала сильное увеличение, затем стабилизация.


Анимация дочерних элементов

Hover-эффект родителя может управлять дочерними элементами:

const card = document.querySelector(".card")
const image = card.querySelector("img")
const title = card.querySelector("h3")

hover(card, () => {
  animate(image, { scale: 1.1 })
  animate(title, { y: -5 })

  return () => {
    animate(image, { scale: 1 })
    animate(title, { y: 0 })
  }
})

Так создаётся комплексный интерактивный эффект.


Использование stagger для hover

Если элемент содержит список, можно добавить последовательность:

import { animate, stagger } from "motion"

hover(menu, () => {
  animate("li", { x: 10 }, {
    delay: stagger(0.05)
  })
})

Каждый элемент реагирует с небольшой задержкой, создавая «волновой» эффект.


Управление производительностью

Hover-анимации выполняются часто, поэтому важно соблюдать оптимизации:

Рекомендации:

  • использовать transform и opacity вместо layout-свойств
  • избегать width, height, top, left
  • минимизировать repaint и reflow
  • использовать will-change при необходимости
.card {
  will-change: transform;
}

Отмена и прерывание анимаций

Motion One автоматически управляет конфликтами, но иногда требуется ручной контроль:

let animation

hover(button, () => {
  animation?.cancel()
  animation = animate(button, { scale: 1.1 })

  return () => {
    animation?.cancel()
    animate(button, { scale: 1 })
  }
})

Это предотвращает накопление анимаций при быстром движении курсора.


Использование spring-анимаций

Для более «живого» эффекта можно применять пружинную физику:

import { spring } from "motion"

hover(button, () => {
  return animate(button, { scale: 1.1 }, {
    easing: spring({ stiffness: 300, damping: 20 })
  })
})

Параметры:

  • stiffness — жёсткость
  • damping — затухание

Комбинирование с CSS

Иногда выгодно комбинировать Motion One и CSS:

.button {
  transition: background 0.2s;
}

.button:hover {
  background: #333;
}
hover(button, () => {
  return animate(button, { scale: 1.1 })
})

CSS отвечает за простые эффекты, Motion One — за сложные.


Hover и доступность

Hover недоступен на сенсорных устройствах, поэтому важно учитывать альтернативы:

  • использовать focus для клавиатуры
  • дублировать поведение через click
  • избегать критически важной информации только в hover
button.addEventListener("focus", () => {
  animate(button, { scale: 1.1 })
})

Типичные паттерны hover-анимаций

1. Поднятие карточки

animate(card, {
  y: -10,
  boxShadow: "0 15px 40px rgba(0,0,0,0.2)"
})

2. Увеличение изображения

animate(img, { scale: 1.1 })

3. Подсветка кнопки

animate(button, {
  backgroundColor: "#000",
  color: "#fff"
})

4. Появление overlay

animate(overlay, { opacity: 1 })

Ошибки при реализации

Частые проблемы:

  • резкие переходы без easing
  • накопление анимаций
  • анимация layout-свойств
  • отсутствие состояния возврата
  • перегруженные эффекты

Архитектура hover-взаимодействий

При масштабировании проекта hover-эффекты лучше организовывать:

  • через отдельные функции
  • с переиспользуемыми конфигурациями
  • с централизованными easing и duration
const hoverScale = (element) => {
  return hover(element, () => {
    return animate(element, { scale: 1.05 }, { duration: 0.2 })
  })
}

Это упрощает поддержку и делает код консистентным.


Интеграция с компонентным подходом

В UI-библиотеках hover-логика часто инкапсулируется:

function createHoverEffect(el) {
  return hover(el, () => {
    return animate(el, { scale: 1.1 })
  })
}

Позволяет повторно использовать поведение без дублирования кода.


Управление временными параметрами

Важную роль играет длительность:

  • 0.1–0.2s — быстрые интерфейсы
  • 0.2–0.4s — стандарт
  • 0.4s — декоративные эффекты

Слишком длинные hover-анимации ухудшают UX.


Итеративная настройка

Тонкая настройка включает:

  • подбор easing
  • регулировку масштаба
  • баланс между скоростью и плавностью
  • тестирование на разных устройствах

Небольшие изменения (например, scale 1.05 → 1.08) сильно влияют на восприятие.


Связь hover с другими состояниями

Hover часто комбинируется с:

  • active
  • focus
  • pressed
button.addEventListener("mousedown", () => {
  animate(button, { scale: 0.95 })
})

button.addEventListener("mouseup", () => {
  animate(button, { scale: 1.1 })
})

Это создаёт полноценную интерактивную систему.


Расширенные эффекты

Параллакс при hover:

hover(card, (event) => {
  const x = event.clientX

  return animate(card, {
    rotateY: x / 50
  })
})

Градиентное движение:

animate(button, {
  backgroundPosition: "100% 0%"
})

Вывод структуры hover-анимации

Типичная схема:

  1. Событие наведения
  2. Запуск анимации
  3. Хранение состояния
  4. Отмена или обратная анимация
  5. Оптимизация производительности

Такая модель обеспечивает предсказуемое и стабильное поведение интерфейса.