Перейти к содержимому

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

Одна из суперсил Svelte — встроенные анимации и переходы. Не нужны библиотеки вроде Framer Motion, не нужен JavaScript для CSS классов. Просто transition:fade — и элемент красиво появляется и исчезает 🪄


В 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 код!


Самый простой — плавное появление/исчезновение через изменение 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)

<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 — смещение по оси X
  • y — смещение по оси Y
  • opacity — начальная/конечная прозрачность
  • duration, delay, easing

<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}

<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

<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}

<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}

Можно задать разные анимации для появления и исчезновения:

<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}
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-манипуляций.

Это МАГИЯ. Элемент как будто “перелетает” из одного места в другое:

<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>

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}

<div
transition:fade
on:introstart={() => console.log('Начало появления')}
on:introend={() => console.log('Конец появления')}
on:outrostart={() => console.log('Начало исчезновения')}
on:outroend={() => console.log('Конец исчезновения')}
>
Контент
</div>

По умолчанию переходы применяются к дочерним элементам тоже. |local ограничивает переход только текущим:

{#if parentVisible}
<div>
<!-- Этот элемент анимируется ТОЛЬКО при изменении его condition -->
<!-- НЕ при изменении parentVisible -->
{#if childVisible}
<div transition:fade|local>
Только мой переход
</div>
{/if}
</div>
{/if}

+layout.svelte
<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}