23. Переходы и анимации
🎨 Переходы и анимации в Solid.js
Заголовок раздела «🎨 Переходы и анимации в Solid.js»Привет! 👋 Solid.js предоставляет несколько мощных способов анимировать интерфейс: от простого переключения CSS-классов до интеграции с браузерным View Transitions API и физически-реалистичными spring-анимациями.
Думай об анимациях в Solid как о слоях: сначала CSS (быстро, просто), потом transition:name для переходов между страницами, потом spring-физика для “живых” ощущений.
🎭 CSS переходы через переключение классов
Заголовок раздела «🎭 CSS переходы через переключение классов»Самый простой способ — добавлять/убирать CSS-классы реактивно:
import { createSignal } from 'solid-js';
function AnimatedCard() { const [visible, setVisible] = createSignal(false); const [expanded, setExpanded] = createSignal(false);
return ( <> <button onClick={() => setVisible(v => !v)}> {visible() ? 'Скрыть' : 'Показать'} </button>
{/* classList — реактивный объект классов */} <div classList={{ 'card': true, 'card--visible': visible(), 'card--expanded': expanded(), }} onClick={() => setExpanded(e => !e)} > Нажми чтобы развернуть </div> </> );}.card { opacity: 0; transform: translateY(10px) scale(0.97); transition: opacity 0.3s ease, transform 0.3s ease;}
.card--visible { opacity: 1; transform: translateY(0) scale(1);}
.card--expanded { height: 200px; transition: height 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); /* spring-like */}🔄 View Transitions API: transition:name
Заголовок раздела «🔄 View Transitions API: transition:name»Solid.js поддерживает директиву transition:name для плавных переходов между DOM-узлами (браузерный View Transitions API):
import { useNavigate } from '@solidjs/router';
// Карточка товара — с именованным переходомfunction ProductCard({ product }: { product: Product }) { const navigate = useNavigate();
return ( <div // transition:name привязывает элемент к именованному переходу // При навигации браузер анимирует переход между двумя элементами с одинаковым именем style={{ "view-transition-name": `product-${product.id}` }} onClick={() => navigate(`/products/${product.id}`)} > <img src={product.image} style={{ "view-transition-name": `product-img-${product.id}` }} /> <h3>{product.name}</h3> </div> );}
// Страница товара — тот же transition name = плавный переход!function ProductPage({ id }: { id: number }) { return ( <div style={{ "view-transition-name": `product-${id}` }}> <img style={{ "view-transition-name": `product-img-${id}` }} /> {/* Содержимое страницы */} </div> );}// router.tsx — включаем View Transitionsimport { Router } from '@solidjs/router';
// View Transitions включаются автоматически в @solidjs/router v0.12+// Или вручную:function AppRouter() { return ( <Router // Включить View Transitions для навигации explicitLinks={false} > {/* роуты */} </Router> );}📋 @solid-primitives/transition-group: анимации списков
Заголовок раздела «📋 @solid-primitives/transition-group: анимации списков»Для анимации добавления/удаления элементов из списков:
import { createSignal, For } from 'solid-js';import { TransitionGroup } from '@solid-primitives/transition-group';
function AnimatedList() { const [items, setItems] = createSignal(['Яблоко', 'Банан', 'Вишня']);
const addItem = () => { const fruits = ['Манго', 'Клубника', 'Черника', 'Груша', 'Апельсин']; const random = fruits[Math.floor(Math.random() * fruits.length)]; setItems(prev => [...prev, random]); };
const removeItem = (name: string) => { setItems(prev => prev.filter(i => i !== name)); };
return ( <div> <button onClick={addItem}>+ Добавить фрукт</button>
<TransitionGroup name="fade-slide"> <For each={items()}> {(item) => ( <div class="list-item" onClick={() => removeItem(item)}> {item} ✕ </div> )} </For> </TransitionGroup> </div> );}/* Enter animation */.fade-slide-enter-active,.fade-slide-exit-active { transition: all 0.3s ease;}
.fade-slide-enter-from,.fade-slide-exit-to { opacity: 0; transform: translateX(-20px);}
/* Move animation (для перегруппировки) */.fade-slide-move { transition: transform 0.3s ease;}🌊 Spring и Tween анимации
Заголовок раздела «🌊 Spring и Tween анимации»import { createSignal } from 'solid-js';// Из @solid-primitives/springimport { createSpring } from '@solid-primitives/spring';
function SpringDemo() { const [target, setTarget] = createSignal(0);
// spring автоматически интерполирует к target значению const position = createSpring(target, { stiffness: 0.15, // жёсткость пружины damping: 0.8, // затухание precision: 0.001, });
return ( <div> <div style={{ transform: `translateX(${position()}px)`, // Обновляется каждый кадр — плавно! }} > 🔵 </div>
<button onClick={() => setTarget(t => t === 0 ? 200 : 0)}> Анимировать </button> </div> );}
// createTween — линейная интерполяция с easingimport { createTween } from '@solid-primitives/tween';
function TweenDemo() { const [target, setTarget] = createSignal(100);
// linear tween за 500ms const animated = createTween(target, { duration: 500, easing: t => t * t });
return ( <div> <div style={{ width: `${animated()}px`, height: '20px', background: 'blue' }} /> <button onClick={() => setTarget(t => t === 100 ? 300 : 100)}> Анимировать ширину </button> </div> );}🎬 motion.dev/solid — Motion One интеграция
Заголовок раздела «🎬 motion.dev/solid — Motion One интеграция»import { createSignal } from 'solid-js';import { motion } from '@motionone/solid';
function MotionDemo() { const [open, setOpen] = createSignal(false);
return ( <> {/* motion.div — декларативные анимации */} <motion.div animate={{ rotate: open() ? 180 : 0, scale: open() ? 1.2 : 1, }} transition={{ duration: 0.3, easing: 'ease-in-out' }} > ▼ </motion.div>
{/* Анимация появления/исчезновения */} <motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: open() ? 1 : 0, y: open() ? 0 : -10 }} transition={{ duration: 0.2 }} > Содержимое раскрывающегося блока </motion.div>
<button onClick={() => setOpen(o => !o)}>Раскрыть</button> </> );}
// Stagger: анимация группы элементов с задержкойimport { inView, animate, stagger } from 'motion';import { onMount } from 'solid-js';
function StaggerList() { let listRef!: HTMLUListElement;
onMount(() => { // Анимируем все li с stagger animate( listRef.querySelectorAll('li'), { opacity: [0, 1], y: [20, 0] }, { delay: stagger(0.1), duration: 0.4 } ); });
return ( <ul ref={listRef}> <li>Элемент 1</li> <li>Элемент 2</li> <li>Элемент 3</li> </ul> );}⚠️ Типичные ловушки
Заголовок раздела «⚠️ Типичные ловушки»// ❌ height: auto — не анимируется в CSS!// transition: height 0.3s — не работает с auto.accordion { height: 0; /* height: auto — нет! */}
// ✅ Используй max-height или clip-path.accordion { max-height: 0; overflow: hidden; transition: max-height 0.3s ease;}.accordion--open { max-height: 500px; /* немного больше максимально возможной высоты */}
// ❌ animation вместо transition для реактивных изменений// animation запускается один раз, transition следит за изменениями
// ✅ Для "trigger on click" используй animation + class toggle.card { animation: none;}.card--bounce { animation: bounce 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) forwards;}
// ❌ Анимировать layout properties (width, height, top, left)// Это вызывает layout reflow — дорого!
// ✅ Анимируй только transform и opacity// transform: translateX/Y/Z, scale, rotate — GPU-ускоренные!