Самый простой способ анимировать элементы — CSS transitions через динамические классы или inline-стили:
import { useState } from 'react'
import styles from './Fade.module.css'
// Fade.module.css:
// .visible { opacity: 1; transform: translateY(0); transition: all 0.3s ease; }
// .hidden { opacity: 0; transform: translateY(10px); }
function FadeIn({ children }) {
const [visible, setVisible] = useState(false)
return (
<div>
<button onClick={() => setVisible(v => !v)}>Переключить</button>
<div className={visible ? styles.visible : styles.hidden}>
{children}
</div>
</div>
)
}Framer Motion — самая мощная библиотека анимаций для React:
npm install framer-motionimport { motion } from 'framer-motion'
// Базовые анимации
function Basic() {
return (
<motion.div
initial={{ opacity: 0, y: -20 }} // начальное состояние
animate={{ opacity: 1, y: 0 }} // целевое состояние
exit={{ opacity: 0, y: 20 }} // при удалении
transition={{ duration: 0.3, ease: 'easeOut' }}
>
Анимированный блок
</motion.div>
)
}Variants позволяют задать несколько состояний и переключаться между ними:
const cardVariants = {
hidden: { opacity: 0, scale: 0.8 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.4, ease: 'easeOut' }
},
hover: { scale: 1.05 },
tap: { scale: 0.95 },
}
function Card({ title }) {
return (
<motion.div
variants={cardVariants}
initial="hidden"
animate="visible"
whileHover="hover"
whileTap="tap"
>
{title}
</motion.div>
)
}
// Stagger: анимация дочерних элементов с задержкой
const listVariants = {
visible: {
transition: { staggerChildren: 0.1 } // каждый ребёнок на 0.1с позже
}
}
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
}
function AnimatedList({ items }) {
return (
<motion.ul variants={listVariants} initial="hidden" animate="visible">
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.text}
</motion.li>
))}
</motion.ul>
)
}import { AnimatePresence, motion } from 'framer-motion'
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
key="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }} // ← работает только с AnimatePresence!
>
<div onClick={onClose} />
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}import { motion, useMotionValue, useTransform } from 'framer-motion'
function DraggableCard() {
const x = useMotionValue(0)
// Поворот пропорционален смещению по X
const rotate = useTransform(x, [-200, 200], [-30, 30])
const opacity = useTransform(x, [-200, 0, 200], [0.5, 1, 0.5])
return (
<motion.div
drag="x"
dragConstraints={{ left: -100, right: 100 }}
style={{ x, rotate, opacity }}
whileDrag={{ scale: 1.1 }}
>
Перетащи меня
</motion.div>
)
}// layoutId: анимирует переход элемента между разными позициями в DOM
function GalleryItem({ item, isExpanded, onClick }) {
return (
<motion.div layoutId={`item-${item.id}`} onClick={onClick}>
<motion.img src={item.src} layoutId={`img-${item.id}`} />
{isExpanded && (
<motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
{item.description}
</motion.p>
)}
</motion.div>
)
}// Встроенные easing
transition={{ ease: 'linear' }} // равномерно
transition={{ ease: 'easeIn' }} // ускорение в начале
transition={{ ease: 'easeOut' }} // замедление в конце
transition={{ ease: 'easeInOut' }} // ускорение и замедление
transition={{ ease: 'anticipate' }} // небольшой откат назад перед движением
transition={{ ease: 'backOut' }} // перелёт за цель с возвратом
transition={{ ease: [0.25, 0.1, 0.25, 1] }} // кубическая кривая БезьеПланировщик анимаций на ванильном JS: keyframes, easing-функции, последовательные и параллельные анимации
// Реализуем планировщик анимаций:
// keyframes, easing, последовательность и параллельность.
// --- Easing функции ---
const easings = {
linear: t => t,
easeIn: t => t * t,
easeOut: t => t * (2 - t),
'ease-in-out': t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
backOut: t => {
const c1 = 1.70158
return 1 + (c1 + 1) * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2)
},
}
// --- Интерполяция значения ---
function lerp(from, to, t) {
return from + (to - from) * t
}
// --- Анимация одного свойства ---
function animateValue(from, to, duration, easingName, onUpdate, onComplete) {
const easeFn = easings[easingName] || easings.linear
const startTime = Date.now()
const frames = []
function tick() {
const elapsed = Date.now() - startTime
const rawT = Math.min(elapsed / duration, 1)
const t = easeFn(rawT)
const value = lerp(from, to, t)
frames.push({ t: rawT.toFixed(2), value: Math.round(value * 100) / 100 })
onUpdate(value)
if (rawT < 1) {
setTimeout(tick, 16) // ~60fps симуляция
} else {
onComplete(frames)
}
}
tick()
}
// --- Планировщик ---
function createAnimationScheduler() {
const animations = []
return {
// Добавить анимацию в очередь
add(name, from, to, duration, easingName = 'linear') {
animations.push({ name, from, to, duration, easingName })
return this // chaining
},
// Запустить ВСЕ параллельно
runParallel() {
console.log('=== Параллельный запуск', animations.length, 'анимаций ===')
const promises = animations.map(anim => {
return new Promise(resolve => {
const samples = []
animateValue(
anim.from, anim.to, anim.duration, anim.easingName,
(v) => samples.push(Math.round(v * 10) / 10),
(frames) => {
console.log('[' + anim.name + '] ' + anim.easingName + ': ' +
anim.from + ' → ' + anim.to + ' за ' + anim.duration + 'мс')
console.log(' Ключевые точки:', samples.filter((_, i) => i % 5 === 0 || i === samples.length - 1).join(', '))
resolve({ name: anim.name, finalValue: anim.to })
}
)
})
})
return Promise.all(promises)
},
// Запустить ПОСЛЕДОВАТЕЛЬНО
async runSequential() {
console.log('=== Последовательный запуск', animations.length, 'анимаций ===')
const results = []
for (const anim of animations) {
await new Promise(resolve => {
animateValue(
anim.from, anim.to, anim.duration, anim.easingName,
() => {},
() => {
console.log('[' + anim.name + '] завершена: ' + anim.from + ' → ' + anim.to)
results.push({ name: anim.name, done: true })
resolve()
}
)
})
}
return results
}
}
}
// --- Демонстрация ---
async function demo() {
// Параллельно
const scheduler1 = createAnimationScheduler()
scheduler1
.add('opacity', 0, 1, 200, 'ease-in-out')
.add('translateY', -20, 0, 250, 'easeOut')
.add('scale', 0.8, 1, 300, 'backOut')
const results = await scheduler1.runParallel()
console.log('Все завершены:', results.map(r => r.name).join(', '))
console.log('')
// Последовательно
const scheduler2 = createAnimationScheduler()
scheduler2
.add('fadeIn', 0, 1, 150, 'easeOut')
.add('slideIn', -100, 0, 200, 'ease-in-out')
await scheduler2.runSequential()
console.log('Последовательность завершена!')
}
demo()Самый простой способ анимировать элементы — CSS transitions через динамические классы или inline-стили:
import { useState } from 'react'
import styles from './Fade.module.css'
// Fade.module.css:
// .visible { opacity: 1; transform: translateY(0); transition: all 0.3s ease; }
// .hidden { opacity: 0; transform: translateY(10px); }
function FadeIn({ children }) {
const [visible, setVisible] = useState(false)
return (
<div>
<button onClick={() => setVisible(v => !v)}>Переключить</button>
<div className={visible ? styles.visible : styles.hidden}>
{children}
</div>
</div>
)
}Framer Motion — самая мощная библиотека анимаций для React:
npm install framer-motionimport { motion } from 'framer-motion'
// Базовые анимации
function Basic() {
return (
<motion.div
initial={{ opacity: 0, y: -20 }} // начальное состояние
animate={{ opacity: 1, y: 0 }} // целевое состояние
exit={{ opacity: 0, y: 20 }} // при удалении
transition={{ duration: 0.3, ease: 'easeOut' }}
>
Анимированный блок
</motion.div>
)
}Variants позволяют задать несколько состояний и переключаться между ними:
const cardVariants = {
hidden: { opacity: 0, scale: 0.8 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.4, ease: 'easeOut' }
},
hover: { scale: 1.05 },
tap: { scale: 0.95 },
}
function Card({ title }) {
return (
<motion.div
variants={cardVariants}
initial="hidden"
animate="visible"
whileHover="hover"
whileTap="tap"
>
{title}
</motion.div>
)
}
// Stagger: анимация дочерних элементов с задержкой
const listVariants = {
visible: {
transition: { staggerChildren: 0.1 } // каждый ребёнок на 0.1с позже
}
}
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
}
function AnimatedList({ items }) {
return (
<motion.ul variants={listVariants} initial="hidden" animate="visible">
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.text}
</motion.li>
))}
</motion.ul>
)
}import { AnimatePresence, motion } from 'framer-motion'
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
key="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }} // ← работает только с AnimatePresence!
>
<div onClick={onClose} />
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}import { motion, useMotionValue, useTransform } from 'framer-motion'
function DraggableCard() {
const x = useMotionValue(0)
// Поворот пропорционален смещению по X
const rotate = useTransform(x, [-200, 200], [-30, 30])
const opacity = useTransform(x, [-200, 0, 200], [0.5, 1, 0.5])
return (
<motion.div
drag="x"
dragConstraints={{ left: -100, right: 100 }}
style={{ x, rotate, opacity }}
whileDrag={{ scale: 1.1 }}
>
Перетащи меня
</motion.div>
)
}// layoutId: анимирует переход элемента между разными позициями в DOM
function GalleryItem({ item, isExpanded, onClick }) {
return (
<motion.div layoutId={`item-${item.id}`} onClick={onClick}>
<motion.img src={item.src} layoutId={`img-${item.id}`} />
{isExpanded && (
<motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
{item.description}
</motion.p>
)}
</motion.div>
)
}// Встроенные easing
transition={{ ease: 'linear' }} // равномерно
transition={{ ease: 'easeIn' }} // ускорение в начале
transition={{ ease: 'easeOut' }} // замедление в конце
transition={{ ease: 'easeInOut' }} // ускорение и замедление
transition={{ ease: 'anticipate' }} // небольшой откат назад перед движением
transition={{ ease: 'backOut' }} // перелёт за цель с возвратом
transition={{ ease: [0.25, 0.1, 0.25, 1] }} // кубическая кривая БезьеПланировщик анимаций на ванильном JS: keyframes, easing-функции, последовательные и параллельные анимации
// Реализуем планировщик анимаций:
// keyframes, easing, последовательность и параллельность.
// --- Easing функции ---
const easings = {
linear: t => t,
easeIn: t => t * t,
easeOut: t => t * (2 - t),
'ease-in-out': t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
backOut: t => {
const c1 = 1.70158
return 1 + (c1 + 1) * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2)
},
}
// --- Интерполяция значения ---
function lerp(from, to, t) {
return from + (to - from) * t
}
// --- Анимация одного свойства ---
function animateValue(from, to, duration, easingName, onUpdate, onComplete) {
const easeFn = easings[easingName] || easings.linear
const startTime = Date.now()
const frames = []
function tick() {
const elapsed = Date.now() - startTime
const rawT = Math.min(elapsed / duration, 1)
const t = easeFn(rawT)
const value = lerp(from, to, t)
frames.push({ t: rawT.toFixed(2), value: Math.round(value * 100) / 100 })
onUpdate(value)
if (rawT < 1) {
setTimeout(tick, 16) // ~60fps симуляция
} else {
onComplete(frames)
}
}
tick()
}
// --- Планировщик ---
function createAnimationScheduler() {
const animations = []
return {
// Добавить анимацию в очередь
add(name, from, to, duration, easingName = 'linear') {
animations.push({ name, from, to, duration, easingName })
return this // chaining
},
// Запустить ВСЕ параллельно
runParallel() {
console.log('=== Параллельный запуск', animations.length, 'анимаций ===')
const promises = animations.map(anim => {
return new Promise(resolve => {
const samples = []
animateValue(
anim.from, anim.to, anim.duration, anim.easingName,
(v) => samples.push(Math.round(v * 10) / 10),
(frames) => {
console.log('[' + anim.name + '] ' + anim.easingName + ': ' +
anim.from + ' → ' + anim.to + ' за ' + anim.duration + 'мс')
console.log(' Ключевые точки:', samples.filter((_, i) => i % 5 === 0 || i === samples.length - 1).join(', '))
resolve({ name: anim.name, finalValue: anim.to })
}
)
})
})
return Promise.all(promises)
},
// Запустить ПОСЛЕДОВАТЕЛЬНО
async runSequential() {
console.log('=== Последовательный запуск', animations.length, 'анимаций ===')
const results = []
for (const anim of animations) {
await new Promise(resolve => {
animateValue(
anim.from, anim.to, anim.duration, anim.easingName,
() => {},
() => {
console.log('[' + anim.name + '] завершена: ' + anim.from + ' → ' + anim.to)
results.push({ name: anim.name, done: true })
resolve()
}
)
})
}
return results
}
}
}
// --- Демонстрация ---
async function demo() {
// Параллельно
const scheduler1 = createAnimationScheduler()
scheduler1
.add('opacity', 0, 1, 200, 'ease-in-out')
.add('translateY', -20, 0, 250, 'easeOut')
.add('scale', 0.8, 1, 300, 'backOut')
const results = await scheduler1.runParallel()
console.log('Все завершены:', results.map(r => r.name).join(', '))
console.log('')
// Последовательно
const scheduler2 = createAnimationScheduler()
scheduler2
.add('fadeIn', 0, 1, 150, 'easeOut')
.add('slideIn', -100, 0, 200, 'ease-in-out')
await scheduler2.runSequential()
console.log('Последовательность завершена!')
}
demo()Создай компонент AnimatedBox, который использует CSS keyframe анимацию через состояние. Компонент должен: отображать квадрат 100x100px с цветом фона, иметь кнопку "Анимировать", при клике на которую запускается анимация (isAnimating = true). Когда isAnimating = true, добавь класс "animate-pulse" к квадрату. Используй inline стили для анимации: при isAnimating добавь animation: "pulse 0.5s ease-in-out". Также добавь @keyframes pulse через тег <style>.
useState(false) для начального состояния. При клике setIsAnimating(true), через setTimeout(500мс) setIsAnimating(false). В boxStyle: animation: isAnimating ? "pulse 0.5s ease-in-out" : "none". В keyframes: scale(1.1) и opacity: 0.7 для 50% точки.