12. Переходы и анимации
🎬 Переходы и Анимации в Svelte
Заголовок раздела «🎬 Переходы и Анимации в Svelte»Одна из суперсил Svelte — встроенные анимации и переходы. Не нужны библиотеки вроде Framer Motion, не нужен JavaScript для CSS классов. Просто transition:fade — и элемент красиво появляется и исчезает 🪄
Зачем это встроено в Svelte?
Заголовок раздела «Зачем это встроено в Svelte?»В React ты пишешь что-то вроде:
// React: нужна библиотекаimport { motion } from 'framer-motion'
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}> Привет!</motion.div>В Svelte:
<script> import { fade } from 'svelte/transition'</script>
{#if visible} <div transition:fade>Привет!</div>{/if}Это не просто синтаксический сахар — Svelte компилирует переходы в оптимальный CSS/JS код!
Встроенные переходы: fade
Заголовок раздела «Встроенные переходы: fade»Самый простой — плавное появление/исчезновение через изменение opacity:
<script> import { fade } from 'svelte/transition' let visible = true</script>
<button on:click={() => visible = !visible}>Toggle</button>
{#if visible} <!-- Стандартный fade --> <div transition:fade> Я плавно появляюсь и исчезаю! </div>
<!-- С параметрами --> <div transition:fade={{ duration: 500, delay: 100 }}> Я медленнее и с задержкой </div>{/if}Параметры fade:
delay— задержка перед началом (мс)duration— продолжительность (мс)easing— функция плавности (по умолчаниюlinear)
fly — полёт с перемещением
Заголовок раздела «fly — полёт с перемещением»<script> import { fly } from 'svelte/transition' let visible = true</script>
{#if visible} <!-- Появляется снизу --> <div transition:fly={{ y: 50, duration: 400 }}> Летит снизу! </div>
<!-- Появляется справа --> <div transition:fly={{ x: 200, duration: 300 }}> Летит справа! </div>
<!-- Диагональный полёт --> <div transition:fly={{ x: -100, y: -50, duration: 500, opacity: 0.5 }}> Летит по диагонали </div>{/if}Параметры fly:
x— смещение по оси Xy— смещение по оси Yopacity— начальная/конечная прозрачностьduration,delay,easing
slide — выдвижение
Заголовок раздела «slide — выдвижение»<script> import { slide } from 'svelte/transition' let expanded = false</script>
<button on:click={() => expanded = !expanded}> {expanded ? '▲' : '▼'} Подробнее</button>
{#if expanded} <div transition:slide={{ duration: 300 }}> <p>Этот контент выдвигается как аккордеон!</p> <p>Очень удобно для FAQ-разделов.</p> <p>Работает за счёт анимации height.</p> </div>{/if}scale — масштабирование
Заголовок раздела «scale — масштабирование»<script> import { scale } from 'svelte/transition' let showModal = false</script>
{#if showModal} <div class="overlay" transition:fade> <div class="modal" transition:scale={{ duration: 250, start: 0.9 }}> <h2>Модальное окно</h2> <button on:click={() => showModal = false}>Закрыть</button> </div> </div>{/if}Параметры scale:
start— начальный масштаб (0 = полностью сжат, 1 = нормальный)opacity— начальная/конечная прозрачностьduration,delay,easing
draw — рисование SVG путей ✍️
Заголовок раздела «draw — рисование SVG путей ✍️»<script> import { draw } from 'svelte/transition' let visible = false</script>
<button on:click={() => visible = !visible}>Draw!</button>
{#if visible} <svg viewBox="0 0 100 100" width="200" height="200"> <!-- Путь "рисуется" от начала до конца! --> <path d="M 10 50 C 10 10, 90 10, 90 50 S 90 90, 10 90" fill="none" stroke="#ff3e00" stroke-width="3" transition:draw={{ duration: 1000, easing: cubicOut }} />
<!-- Галочка --> <polyline points="20,50 40,70 80,30" fill="none" stroke="#ffba00" stroke-width="4" transition:draw={{ duration: 500 }} /> </svg>{/if}blur — размытие
Заголовок раздела «blur — размытие»<script> import { blur } from 'svelte/transition' let focused = false</script>
{#if focused} <!-- Появляется из размытия --> <div transition:blur={{ amount: 10, duration: 400 }}> Фокус! </div>{/if}Параметры переходов
Заголовок раздела «Параметры переходов»Все встроенные переходы принимают общие параметры:
interface TransitionParams { delay?: number // Задержка перед началом (мс), default: 0 duration?: number // Длительность (мс), default: 400 easing?: (t: number) => number // Функция плавности}<script> import { fade } from 'svelte/transition' import { elasticOut, bounceOut, cubicInOut } from 'svelte/easing'</script>
{#if visible} <div transition:fade={{ duration: 1000, easing: elasticOut }}> Резиновый эффект! </div>{/if}in: и out: — раздельные переходы
Заголовок раздела «in: и out: — раздельные переходы»Можно задать разные анимации для появления и исчезновения:
<script> import { fade, fly, slide } from 'svelte/transition'</script>
{#if visible} <!-- Появляется снизу, исчезает вверх --> <div in:fly={{ y: 50 }} out:fly={{ y: -50 }}> Летит снизу, улетает вверх </div>
<!-- Появляется с fade, исчезает с slide --> <div in:fade out:slide> Разные переходы! </div>
<!-- Появляется быстро, исчезает медленно --> <div in:fade={{ duration: 100 }} out:fade={{ duration: 800 }} > Быстро появляется, медленно уходит </div>{/if}animate:flip — анимация перестановки элементов 🔄
Заголовок раздела «animate:flip — анимация перестановки элементов 🔄»Когда элементы списка перемещаются (не добавляются/удаляются), используй animate:flip:
<script> import { flip } from 'svelte/animate' import { fade } from 'svelte/transition'
let items = [ { id: 1, name: 'Яша' }, { id: 2, name: 'Петя' }, { id: 3, name: 'Маша' }, ]
function shuffle() { items = [...items].sort(() => Math.random() - 0.5) }
function removeItem(id: number) { items = items.filter(item => item.id !== id) }</script>
<button on:click={shuffle}>🔀 Перемешать</button>
<ul> {#each items as item (item.id)} <!-- animate:flip — плавное перемещение при сортировке --> <!-- transition:fade — появление/исчезновение --> <li animate:flip={{ duration: 300 }} transition:fade> {item.name} <button on:click={() => removeItem(item.id)}>✕</button> </li> {/each}</ul>flip = First Last Invert Play — техника анимации перемещения.
Кастомный переход
Заголовок раздела «Кастомный переход»Если встроенных не хватает — пиши свой! Переход — это просто функция:
import type { TransitionConfig } from 'svelte/transition'
// Переход: "печатная машинка"function typewriter(node: Element, { speed = 50 }: { speed?: number }): TransitionConfig { const text = node.textContent || ''
return { duration: text.length * speed, tick: (t) => { // t идёт от 0 до 1 const i = Math.round(text.length * t) node.textContent = text.slice(0, i) } }}<script> let visible = false</script>
{#if visible} <p transition:typewriter={{ speed: 30 }}> Привет! Я печатаюсь как на машинке! 🖨️ </p>{/if}Переход через CSS
Заголовок раздела «Переход через CSS»import { cubicOut } from 'svelte/easing'import type { TransitionConfig } from 'svelte/transition'
// Переход: вращение + масштабfunction spinIn(node: Element, { duration = 400 }: { duration?: number }): TransitionConfig { return { duration, css: (t) => { // t: 0 → 1 при появлении, 1 → 0 при исчезновении const eased = cubicOut(t) return ` transform: rotate(${(1 - eased) * 360}deg) scale(${eased}); opacity: ${eased}; ` } }}Разница между tick и css:css: → Генерирует CSS keyframes (гладко, GPU)tick: → JavaScript на каждом кадре (гибко, но медленнее)
Используй css когда можешь! Только tick для DOM-манипуляций.crossfade — переход между элементами ✨
Заголовок раздела «crossfade — переход между элементами ✨»Это МАГИЯ. Элемент как будто “перелетает” из одного места в другое:
<script> import { crossfade } from 'svelte/transition' import { quintOut } from 'svelte/easing'
const [send, receive] = crossfade({ duration: 400, easing: quintOut, fallback: fly.bind(null, { y: 200, duration: 300 }), })
let leftItems = ['Задача 1', 'Задача 2', 'Задача 3'] let rightItems: string[] = []
function moveToRight(item: string) { leftItems = leftItems.filter(i => i !== item) rightItems = [...rightItems, item] }
function moveToLeft(item: string) { rightItems = rightItems.filter(i => i !== item) leftItems = [...leftItems, item] }</script>
<div class="columns"> <div class="column"> <h3>К выполнению</h3> {#each leftItems as item (item)} <!-- send: "отправляем" элемент с ключом item --> <div in:receive={{ key: item }} out:send={{ key: item }} on:click={() => moveToRight(item)} > {item} </div> {/each} </div>
<div class="column"> <h3>Выполнено</h3> {#each rightItems as item (item)} <!-- receive: "принимаем" элемент с тем же ключом --> <div in:receive={{ key: item }} out:send={{ key: item }} on:click={() => moveToLeft(item)} > ✅ {item} </div> {/each} </div></div>svelte/easing — функции плавности
Заголовок раздела «svelte/easing — функции плавности»import { // Линейная linear,
// Sinusoidal sineIn, sineOut, sineInOut,
// Квадратичная quadIn, quadOut, quadInOut,
// Кубическая cubicIn, cubicOut, cubicInOut,
// Степенная quartIn, quartOut, quartInOut, quintIn, quintOut, quintInOut,
// Экспоненциальная expoIn, expoOut, expoInOut,
// Круговая circIn, circOut, circInOut,
// Пружинная (выходит за 1!) backIn, backOut, backInOut,
// Упругая (осциллирует) elasticIn, elasticOut, elasticInOut,
// Отскок bounceIn, bounceOut, bounceInOut,} from 'svelte/easing'Как читать названия:In: Начало — медленно, конец — быстро (разгон)Out: Начало — быстро, конец — медленно (торможение)InOut: Разгон + торможение (плавно)
Рекомендации:✅ cubicOut — стандарт Material Design, для появления элементов✅ quintOut — более выразительно для акцентных переходов✅ elasticOut — игривый "пружинный" эффект✅ backOut — небольшой "overshooting" для кнопок❌ linear — выглядит механически для UIУправление переходами
Заголовок раздела «Управление переходами»<script> import { fade } from 'svelte/transition'
// Деактивировать все переходы (например для тестов) // Передаём duration: 0 let prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches</script>
{#if visible} <div transition:fade={{ duration: prefersReducedMotion ? 0 : 300 }}> Уважаем пользователей с чувствительностью к движению! </div>{/if}Transition Events
Заголовок раздела «Transition Events»<div transition:fade on:introstart={() => console.log('Начало появления')} on:introend={() => console.log('Конец появления')} on:outrostart={() => console.log('Начало исчезновения')} on:outroend={() => console.log('Конец исчезновения')}> Контент</div>local: переход только текущего элемента
Заголовок раздела «local: переход только текущего элемента»По умолчанию переходы применяются к дочерним элементам тоже. |local ограничивает переход только текущим:
{#if parentVisible} <div> <!-- Этот элемент анимируется ТОЛЬКО при изменении его condition --> <!-- НЕ при изменении parentVisible --> {#if childVisible} <div transition:fade|local> Только мой переход </div> {/if} </div>{/if}Паттерн: Page Transitions (SvelteKit)
Заголовок раздела «Паттерн: Page Transitions (SvelteKit)»<script> import { fade } from 'svelte/transition' import { navigating } from '$app/stores'</script>
{#key $navigating?.to?.url.pathname} <div transition:fade={{ duration: 200 }}> <slot /> </div>{/key}