Skip to content

Animations et feedback

Polir votre expérience avec animations fluides, sons réactifs et effets visuels. Ces détails transforment une interaction fonctionnelle en expérience mémorable.

Ce que vous allez apprendre :

  • Transitions Svelte (fade, slide, fly)
  • Animations CSS personnalisées
  • Sons et audio réactif
  • Particules et effets visuels

L’animation n’est pas décorative. Elle guide l’œil, confirme l’action, crée du rythme et porte l’émotion.

Animation mal utilisée : Distrait, ralentit, frustre
Animation bien utilisée : Guide, rassure, enchante

Svelte fournit des transitions prêtes à l’emploi pour animer l’apparition/disparition d’éléments.

<script>
import { fade } from 'svelte/transition';
let visible = $state(false);
</script>
<button on:click={() => visible = !visible}>
Toggle
</button>
{#if visible}
<p in:fade={{ duration: 300 }}>
J'apparais en fondu
</p>
{/if}
<script>
import { slide } from 'svelte/transition';
let expanded = $state(false);
</script>
<button on:click={() => expanded = !expanded}>
{expanded ? 'Réduire' : 'Développer'}
</button>
{#if expanded}
<div in:slide={{ duration: 300 }}>
<p>Contenu qui glisse depuis le haut</p>
</div>
{/if}
<script>
import { fly } from 'svelte/transition';
let notifications = $state([]);
function addNotification() {
const id = Date.now();
notifications = [...notifications, { id, text: 'Nouvelle notification' }];
// Auto-retrait après 3s
setTimeout(() => {
notifications = notifications.filter(n => n.id !== id);
}, 3000);
}
</script>
<button on:click={addNotification}>
Ajouter notification
</button>
<div class="notifications">
{#each notifications as notif (notif.id)}
<div
class="notif"
in:fly={{ x: 300, duration: 300 }}
out:fly={{ x: 300, duration: 200 }}
>
{notif.text}
</div>
{/each}
</div>
<style>
.notifications {
position: fixed;
top: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.notif {
padding: 1rem;
background: #333;
color: white;
border-radius: 4px;
min-width: 200px;
}
</style>
<script>
import { fade, fly } from 'svelte/transition';
</script>
{#if visible}
<div
in:fly={{ y: 20, duration: 300 }}
out:fade={{ duration: 200 }}
>
Entre en volant, sort en fondu
</div>
{/if}
<script>
import { fly } from 'svelte/transition';
import { elasticOut } from 'svelte/easing';
</script>
<div
in:fly={{ y: -50, duration: 800, easing: elasticOut }}
>
Animation avec rebond
</div>

Easing disponibles :

  • linear : vitesse constante
  • cubicOut : ralentit à la fin (par défaut)
  • elasticOut : rebond élastique
  • bounceOut : rebondit
  • backOut : recul puis avance

Pour des animations continues ou plus complexes, CSS offre un contrôle précis.

<button class="hover-btn">
Survolez-moi
</button>
<style>
.hover-btn {
padding: 1rem 2rem;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.hover-btn:hover {
background: #45a049;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.hover-btn:active {
transform: scale(0.98);
}
</style>
<div class="pulse">
Point d'intérêt
</div>
<style>
.pulse {
padding: 1rem;
background: #ff6b6b;
color: white;
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
</style>
<script>
import { onMount } from 'svelte';
let scrollProgress = 0;
onMount(() => {
function handleScroll() {
const winScroll = document.documentElement.scrollTop;
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
scrollProgress = (winScroll / height) * 100;
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
});
</script>
<div class="progress-bar" style="width: {scrollProgress}%"></div>
<style>
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #ff6b6b, #4CAF50);
transition: width 0.1s ease;
z-index: 1000;
}
</style>
<script>
import { onMount } from 'svelte';
let offsetY = $state(0);
onMount(() => {
function handleScroll() {
offsetY = window.pageYOffset;
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
});
</script>
<div
class="parallax-bg"
style="transform: translateY({offsetY * 0.5}px);"
>
<!-- Contenu en arrière-plan -->
</div>

Le son renforce l’interaction. Un clic qui fait un bruit devient plus satisfaisant.

<script>
let audio;
function playSound() {
if (audio) {
audio.currentTime = 0; // Redémarre si déjà en cours
audio.play();
}
}
</script>
<audio bind:this={audio} src="/sounds/click.mp3"></audio>
<button on:click={playSound}>
Clic sonore
</button>
<script>
function playFeedback(type) {
const sounds = {
success: '/sounds/success.mp3',
error: '/sounds/error.mp3',
click: '/sounds/click.mp3'
};
const audio = new Audio(sounds[type]);
audio.play();
}
</script>
<button on:click={() => playFeedback('success')}>
Succès
</button>
<button on:click={() => playFeedback('error')}>
Erreur
</button>
<script>
import { onMount } from 'svelte';
let audioContext;
let oscillator;
let gainNode;
onMount(() => {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
gainNode = audioContext.createGain();
gainNode.connect(audioContext.destination);
gainNode.gain.value = 0.3; // Volume
});
function playTone(frequency) {
if (oscillator) {
oscillator.stop();
}
oscillator = audioContext.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.value = frequency;
oscillator.connect(gainNode);
oscillator.start();
// Arrêt après 200ms
setTimeout(() => oscillator.stop(), 200);
}
</script>
<div class="keyboard">
<button on:click={() => playTone(261.63)}>Do</button>
<button on:click={() => playTone(293.66)}></button>
<button on:click={() => playTone(329.63)}>Mi</button>
<button on:click={() => playTone(349.23)}>Fa</button>
<button on:click={() => playTone(392.00)}>Sol</button>
</div>
<script>
function playSubtle() {
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = 800;
osc.type = 'sine';
gain.gain.setValueAtTime(0.1, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.1);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.1);
}
</script>
<button on:click={playSubtle}>
Feedback subtil
</button>

Les particules ajoutent du dynamisme et de la magie à une expérience.

<script>
import { onMount } from 'svelte';
let canvas;
let ctx;
let particles = [];
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = Math.random() * 3 + 1;
this.speedX = (Math.random() - 0.5) * 2;
this.speedY = (Math.random() - 0.5) * 2;
this.life = 1;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.life -= 0.02;
}
draw() {
ctx.fillStyle = `rgba(255, 255, 255, ${this.life})`;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
onMount(() => {
ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function animate() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
particles = particles.filter(p => p.life > 0);
particles.forEach(p => {
p.update();
p.draw();
});
requestAnimationFrame(animate);
}
animate();
});
function handleMouseMove(e) {
for (let i = 0; i < 3; i++) {
particles.push(new Particle(e.clientX, e.clientY));
}
}
</script>
<canvas
bind:this={canvas}
on:mousemove={handleMouseMove}
></canvas>
<style>
canvas {
position: fixed;
top: 0;
left: 0;
pointer-events: none;
z-index: 1000;
}
</style>
<script>
import { onMount } from 'svelte';
let canvas;
onMount(async () => {
// Import dynamique
const confetti = (await import('canvas-confetti')).default;
const myConfetti = confetti.create(canvas, { resize: true });
return () => {
myConfetti.reset();
};
});
function celebrate() {
const myConfetti = canvas.__confetti;
myConfetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 }
});
}
</script>
<button on:click={celebrate}>
🎉 Célébrer
</button>
<canvas bind:this={canvas}></canvas>
<script>
let highlighted = $state(false);
function highlight() {
highlighted = true;
setTimeout(() => highlighted = false, 1000);
}
</script>
<div
class="card"
class:highlight={highlighted}
on:click={highlight}
role="button"
tabindex="0"
>
Cliquez pour surligner
</div>
<style>
.card {
padding: 2rem;
background: white;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.card.highlight {
border-color: gold;
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
animation: pulse-border 1s ease;
}
@keyframes pulse-border {
0%, 100% {
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
}
50% {
box-shadow: 0 0 40px rgba(255, 215, 0, 0.8);
}
}
</style>

Privilégier transform et opacity (accélération GPU) :

/* ✅ Rapide */
transform: translateX(100px);
opacity: 0.5;
/* ❌ Lent */
left: 100px;
width: 200px;

Respecter prefers-reduced-motion :

<script>
import { onMount } from 'svelte';
let scrollProgress = $state(0);
onMount(() => {
```svelte
<script>
import { onMount } from 'svelte';
let prefersReducedMotion = $state(false);
onMount(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
prefersReducedMotion = mediaQuery.matches;
});
</script>
{#if visible}
<div
in:fade={{ duration: prefersReducedMotion ? 0 : 300 }}
>
Contenu animé (ou pas)
</div>
{/if}

Ou via CSS :

@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}

L’animation doit confirmer l’action immédiatement, pas après un délai.

<!-- ✅ Bon : feedback instantané -->
<button
on:click={handleClick}
class:clicked={isClicked}
>
Cliquer
</button>
<!-- ❌ Mauvais : délai avant feedback -->
<button on:click={() => setTimeout(handleClick, 500)}>
Cliquer
</button>

Créez une interface qui combine :

  • Transitions Svelte pour les apparitions/disparitions
  • Animation CSS au survol
  • Son de feedback sur action
  • Un effet visuel (particules ou highlight)

Exemples de concept :

  • Tableau de bord avec notifications animées
  • Jeu simple avec feedback visuel et sonore
  • Interface de vote avec célébration
  • Générateur qui “crée” visuellement
  • ✅ Utiliser les transitions Svelte (fade, slide, fly)
  • ✅ Créer des animations CSS (hover, keyframes, scroll)
  • ✅ Intégrer du son (simple et Web Audio API)
  • ✅ Ajouter des effets visuels (canvas, particules)
  • ✅ Respecter les bonnes pratiques (performance, accessibilité)