Cours Next.js · Module 3: Styles et optimisation visuelle
Animations et transitions dans l'interface utilisateur avec Next.js 16
Dans cette leçon10
À Metropolis Quantum, rien n'est immobile. Les véhicules se déplacent en douceur entre les quartiers, les affichages holographiques changent par des transitions élégantes, et les éléments interactifs de la ville répondent aux habitants par des animations subtiles mais significatives. Ce dynamisme ne procure pas seulement un attrait visuel : il améliore avant tout l'utilisabilité et l'intuitivité des systèmes de la ville.
De même, dans les applications web, des animations et des transitions bien conçues peuvent avoir un impact significatif sur l'expérience utilisateur, rendant l'interface plus naturelle, informative et agréable à utiliser. Dans ce chapitre, nous découvrirons différentes techniques d'implémentation des animations et transitions dans les applications Next.js 16.
Types d'animations dans les interfaces web
Avant de passer aux implémentations concrètes, il vaut la peine de connaître les différentes catégories d'animations que l'on trouve dans les interfaces web :
1. Animations de micro-interactions
Des animations subtiles répondant aux actions de l'utilisateur, par ex. :
- Les effets de survol sur les boutons
- Les animations de clic
- Les indicateurs de chargement
- Les changements d'état des formulaires
2. Animations de transition d'état
Des transitions fluides entre les différents états d'un composant :
- Les menus déroulants
- L'agrandissement/réduction des éléments
- Les changements de visibilité des éléments
- La bascule de mode (clair/sombre)
3. Animations de navigation
Des animations accompagnant les changements de page ou de vue :
- Les transitions de page
- Le changement d'onglet
- Le défilement de la page
- Les effets de parallaxe
4. Animations narratives
Des animations plus complexes présentant une histoire ou un processus :
- Les séquences d'onboarding
- Les illustrations de processus
- Les infographies animées
- Le storytelling visuel
Implémenter des animations dans React et Next.js 16
Dans l'écosystème React et Next.js, nous disposons de nombreuses méthodes pour implémenter des animations :
1. CSS et CSS transitions
La manière la plus simple et la plus performante d'animer des changements simples :
1/* app/globals.css */
2.button {
3 background-color: #3a86ff;
4 color: white;
5 padding: 8px 16px;
6 border-radius: 4px;
7 transition: transform 0.2s ease, background-color 0.2s ease;
8}
9
10.button:hover {
11 transform: translateY(-2px);
12 background-color: #2563eb;
13}2. Animations CSS
Pour des animations plus complexes avec des cycles définis :
1/* app/globals.css */
2@keyframes fadeIn {
3 from {
4 opacity: 0;
5 transform: translateY(20px);
6 }
7 to {
8 opacity: 1;
9 transform: translateY(0);
10 }
11}
12
13.card {
14 animation: fadeIn 0.5s ease forwards;
15}
16
17/* Sequential animations for a list of elements */
18.card:nth-child(1) { animation-delay: 0.1s; }
19.card:nth-child(2) { animation-delay: 0.2s; }
20.card:nth-child(3) { animation-delay: 0.3s; }3. React Hooks et useState
Pour des animations dépendant des états React :
1// components/ExpandablePanel.tsx
2'use client';
3
4import { useState } from 'react';
5import styles from './ExpandablePanel.module.css';
6
7export function ExpandablePanel({ title, children }) {
8 const [isExpanded, setIsExpanded] = useState(false);
9
10 return (
11 <div className={styles.panel}>
12 <button
13 className={styles.header}
14 onClick={() => setIsExpanded(!isExpanded)}
15 >
16 <h3>{title}</h3>
17 <span className={`${styles.icon} ${isExpanded ? styles.iconExpanded : ''}`}>
18 ▼
19 </span>
20 </button>
21 <div className={`
22 ${styles.content}
23 ${isExpanded ? styles.contentExpanded : styles.contentCollapsed}
24 `}>
25 {children}
26 </div>
27 </div>
28 );
29}1/* components/ExpandablePanel.module.css */
2.panel {
3 border: 1px solid #ddd;
4 border-radius: 8px;
5 overflow: hidden;
6 margin-bottom: 16px;
7}
8
9.header {
10 display: flex;
11 justify-content: space-between;
12 align-items: center;
13 padding: 16px;
14 background-color: #f9f9f9;
15 cursor: pointer;
16 width: 100%;
17 text-align: left;
18 border: none;
19}
20
21.icon {
22 transition: transform 0.3s ease;
23}
24
25.iconExpanded {
26 transform: rotate(180deg);
27}
28
29.content {
30 padding: 0 16px;
31 transition: all 0.3s ease;
32 overflow: hidden;
33}
34
35.contentExpanded {
36 max-height: 500px; /* Adjust to your needs */
37 padding: 16px;
38 opacity: 1;
39}
40
41.contentCollapsed {
42 max-height: 0;
43 padding-top: 0;
44 padding-bottom: 0;
45 opacity: 0;
46}4. React Transition Group
Une bibliothèque qui simplifie la gestion des transitions d'éléments dans React :
1npm install react-transition-group1// components/Notification.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import { CSSTransition } from 'react-transition-group';
6import styles from './Notification.module.css';
7
8interface NotificationProps {
9 message: string;
10 type?: 'success' | 'error' | 'info';
11 duration?: number;
12}
13
14export function Notification({ message, type = 'info', duration = 3000 }: NotificationProps) {
15 const [show, setShow] = useState(true);
16
17 useEffect(() => {
18 const timer = setTimeout(() => {
19 setShow(false);
20 }, duration);
21
22 return () => clearTimeout(timer);
23 }, [duration]);
24
25 return (
26 <CSSTransition
27 in={show}
28 timeout={300}
29 classNames={{
30 enter: styles.notificationEnter,
31 enterActive: styles.notificationEnterActive,
32 exit: styles.notificationExit,
33 exitActive: styles.notificationExitActive,
34 }}
35 unmountOnExit
36 >
37 <div className={`${styles.notification} ${styles[type]}`}>
38 {message}
39 </div>
40 </CSSTransition>
41 );
42}1/* components/Notification.module.css */
2.notification {
3 position: fixed;
4 bottom: 20px;
5 right: 20px;
6 padding: 16px;
7 border-radius: 8px;
8 color: white;
9 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
10 z-index: 1000;
11}
12
13.success {
14 background-color: #10b981;
15}
16
17.error {
18 background-color: #ef4444;
19}
20
21.info {
22 background-color: #3b82f6;
23}
24
25.notificationEnter {
26 opacity: 0;
27 transform: translateY(20px);
28}
29
30.notificationEnterActive {
31 opacity: 1;
32 transform: translateY(0);
33 transition: opacity 300ms, transform 300ms;
34}
35
36.notificationExit {
37 opacity: 1;
38}
39
40.notificationExitActive {
41 opacity: 0;
42 transform: translateY(20px);
43 transition: opacity 300ms, transform 300ms;
44}5. Framer Motion
Framer Motion est une puissante bibliothèque d'animation pour React qui simplifie la création d'animations complexes :
1npm install framer-motion1// components/Card.tsx
2'use client';
3
4import { motion } from 'framer-motion';
5import styles from './Card.module.css';
6
7interface CardProps {
8 title: string;
9 description: string;
10 imageUrl: string;
11 index: number;
12}
13
14export function Card({ title, description, imageUrl, index }: CardProps) {
15 return (
16 <motion.div
17 className={styles.card}
18 initial={{ opacity: 0, y: 20 }}
19 animate={{ opacity: 1, y: 0 }}
20 transition={{
21 duration: 0.5,
22 delay: index * 0.1,
23 ease: "easeOut"
24 }}
25 whileHover={{
26 scale: 1.05,
27 boxShadow: "0 10px 30px rgba(0, 0, 0, 0.15)"
28 }}
29 >
30 <div className={styles.imageContainer}>
31 <img src={imageUrl} alt={title} />
32 </div>
33 <div className={styles.content}>
34 <h3>{title}</h3>
35 <p>{description}</p>
36 </div>
37 </motion.div>
38 );
39}6. CSS Modules avec Next.js
Next.js permet de combiner facilement les CSS Modules avec les composants :
1// components/Button.tsx
2'use client';
3
4import { useState } from 'react';
5import styles from './Button.module.css';
6
7interface ButtonProps {
8 children: React.ReactNode;
9 onClick?: () => void;
10}
11
12export function Button({ children, onClick }: ButtonProps) {
13 const [isPressed, setIsPressed] = useState(false);
14
15 const handleClick = () => {
16 setIsPressed(true);
17 setTimeout(() => setIsPressed(false), 200);
18 onClick && onClick();
19 };
20
21 return (
22 <button
23 className={`${styles.button} ${isPressed ? styles.pressed : ''}`}
24 onClick={handleClick}
25 >
26 {children}
27 </button>
28 );
29}1/* components/Button.module.css */
2.button {
3 background-color: #3a86ff;
4 color: white;
5 border: none;
6 border-radius: 4px;
7 padding: 10px 20px;
8 font-size: 16px;
9 cursor: pointer;
10 transition: all 0.2s ease;
11 position: relative;
12 overflow: hidden;
13}
14
15.button:hover {
16 background-color: #2563eb;
17}
18
19.button:before {
20 content: '';
21 position: absolute;
22 top: 50%;
23 left: 50%;
24 width: 0;
25 height: 0;
26 background-color: rgba(255, 255, 255, 0.3);
27 border-radius: 50%;
28 transform: translate(-50%, -50%);
29 transition: width 0.4s ease-out, height 0.4s ease-out;
30}
31
32.button:hover:before {
33 width: 300px;
34 height: 300px;
35}
36
37.pressed {
38 transform: scale(0.98);
39}Animations de transition de page dans Next.js 16
Next.js 16 utilise React 19.2 : en plus des techniques classiques comme les animations CSS ou Framer Motion, tu peux utiliser le composant <ViewTransition> de React. Dans l'App Router, il fonctionne sans aucune configuration, car chaque navigation est une transition, et les navigateurs qui ne prennent pas en charge la View Transitions API affichent simplement la nouvelle page sans animation.
1. Transitions de base en CSS
1// app/layout.tsx
2export default function RootLayout({
3 children,
4}: {
5 children: React.ReactNode;
6}) {
7 return (
8 <html lang="pl">
9 <body>
10 <main className="page-transition">
11 {children}
12 </main>
13 </body>
14 </html>
15 );
16}1/* app/globals.css */
2.page-transition {
3 animation: fadeIn 0.5s ease-in-out;
4}
5
6@keyframes fadeIn {
7 from {
8 opacity: 0;
9 transform: translateY(20px);
10 }
11 to {
12 opacity: 1;
13 transform: translateY(0);
14 }
15}2. Transitions avancées avec Framer Motion
1// components/PageTransition.tsx
2'use client';
3
4import { motion } from 'framer-motion';
5
6const variants = {
7 hidden: { opacity: 0, x: -200, y: 0 },
8 enter: { opacity: 1, x: 0, y: 0 },
9 exit: { opacity: 0, x: 0, y: -100 },
10};
11
12export function PageTransition({ children }: { children: React.ReactNode }) {
13 return (
14 <motion.div
15 variants={variants}
16 initial="hidden"
17 animate="enter"
18 exit="exit"
19 transition={{ type: 'linear', duration: 0.5 }}
20 >
21 {children}
22 </motion.div>
23 );
24}1// app/page.tsx
2import { PageTransition } from '@/components/PageTransition';
3
4export default function HomePage() {
5 return (
6 <PageTransition>
7 <h1>Bienvenue à Metropolis Quantum</h1>
8 {/* Rest of content */}
9 </PageTransition>
10 );
11}3. Transitions avec next/navigation
1// components/Navigation.tsx
2'use client';
3
4import { useState } from 'react';
5import { useRouter } from 'next/navigation';
6import Link from 'next/link';
7import styles from './Navigation.module.css';
8
9export function Navigation() {
10 const router = useRouter();
11 const [isNavigating, setIsNavigating] = useState(false);
12
13 const handleNavigate = (href: string) => {
14 setIsNavigating(true);
15 setTimeout(() => {
16 router.push(href);
17 }, 500); // Give time for exit animation
18 };
19
20 return (
21 <nav className={`${styles.nav} ${isNavigating ? styles.navigating : ''}`}>
22 <ul>
23 <li>
24 <a onClick={() => handleNavigate('/')}>Home</a>
25 </li>
26 <li>
27 <a onClick={() => handleNavigate('/about')}>O nas</a>
28 </li>
29 <li>
30 <a onClick={() => handleNavigate('/services')}>Services</a>
31 </li>
32 <li>
33 <a onClick={() => handleNavigate('/contact')}>Kontakt</a>
34 </li>
35 </ul>
36 </nav>
37 );
38}1/* components/Navigation.module.css */
2.nav {
3 transition: opacity 0.5s ease;
4}
5
6.navigating {
7 opacity: 0.5;
8 pointer-events: none;
9}Implémenter des animations d'interface complexes avec Framer Motion
Framer Motion est particulièrement utile pour créer des animations d'interface complexes et interactives. Regardons quelques exemples :
1. Menu mobile animé
1// components/MobileMenu.tsx
2'use client';
3
4import { useState } from 'react';
5import { motion, AnimatePresence } from 'framer-motion';
6import Link from 'next/link';
7import styles from './MobileMenu.module.css';
8
9export function MobileMenu() {
10 const [isOpen, setIsOpen] = useState(false);
11
12 const menuVariants = {
13 closed: {
14 opacity: 0,
15 x: '100%',
16 transition: {
17 type: 'spring',
18 stiffness: 400,
19 damping: 40,
20 },
21 },
22 open: {
23 opacity: 1,
24 x: 0,
25 transition: {
26 type: 'spring',
27 stiffness: 400,
28 damping: 40,
29 staggerChildren: 0.1,
30 delayChildren: 0.2,
31 },
32 },
33 };
34
35 const menuItemVariants = {
36 closed: { opacity: 0, x: 50 },
37 open: { opacity: 1, x: 0 },
38 };
39
40 const buttonVariants = {
41 closed: { rotate: 0 },
42 open: { rotate: 90 },
43 };
44
45 return (
46 <div className={styles.mobileMenuContainer}>
47 <motion.button
48 className={styles.menuButton}
49 onClick={() => setIsOpen(!isOpen)}
50 variants={buttonVariants}
51 animate={isOpen ? 'open' : 'closed'}
52 transition={{ duration: 0.3 }}
53 >
54 <div className={styles.hamburger}>
55 <span className={`${styles.line} ${isOpen ? styles.line1Open : ''}`}></span>
56 <span className={`${styles.line} ${isOpen ? styles.line2Open : ''}`}></span>
57 <span className={`${styles.line} ${isOpen ? styles.line3Open : ''}`}></span>
58 </div>
59 </motion.button>
60
61 <AnimatePresence>
62 {isOpen && (
63 <motion.div
64 className={styles.menuOverlay}
65 variants={menuVariants}
66 initial="closed"
67 animate="open"
68 exit="closed"
69 >
70 <nav className={styles.mobileNav}>
71 <ul>
72 {['Home', 'About', 'Services', 'Contact'].map((item) => (
73 <motion.li key={item} variants={menuItemVariants}>
74 <Link href={item === 'Home' ? '/' : `/${item.toLowerCase()}`}>
75 {item}
76 </Link>
77 </motion.li>
78 ))}
79 </ul>
80 </nav>
81 </motion.div>
82 )}
83 </AnimatePresence>
84 </div>
85 );
86}1/* components/MobileMenu.module.css */
2.mobileMenuContainer {
3 position: fixed;
4 top: 20px;
5 right: 20px;
6 z-index: 1000;
7}
8
9.menuButton {
10 background-color: #3a86ff;
11 border: none;
12 width: 50px;
13 height: 50px;
14 border-radius: 50%;
15 display: flex;
16 align-items: center;
17 justify-content: center;
18 cursor: pointer;
19 z-index: 1001;
20 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
21}
22
23.hamburger {
24 width: 24px;
25 height: 18px;
26 position: relative;
27}
28
29.line {
30 position: absolute;
31 width: 100%;
32 height: 2px;
33 background-color: white;
34 transition: all 0.3s ease;
35}
36
37.line:nth-child(1) {
38 top: 0;
39}
40
41.line:nth-child(2) {
42 top: 8px;
43}
44
45.line:nth-child(3) {
46 top: 16px;
47}
48
49.line1Open {
50 top: 8px;
51 transform: rotate(45deg);
52}
53
54.line2Open {
55 opacity: 0;
56}
57
58.line3Open {
59 top: 8px;
60 transform: rotate(-45deg);
61}
62
63.menuOverlay {
64 position: fixed;
65 top: 0;
66 right: 0;
67 width: 300px;
68 height: 100vh;
69 background-color: white;
70 box-shadow: -5px 0 15px rgba(0, 0, 0, 0.1);
71 z-index: 1000;
72}
73
74.mobileNav {
75 padding: 80px 40px;
76}
77
78.mobileNav ul {
79 list-style: none;
80 padding: 0;
81 margin: 0;
82}
83
84.mobileNav li {
85 margin-bottom: 20px;
86}
87
88.mobileNav a {
89 font-size: 24px;
90 color: #333;
91 text-decoration: none;
92 transition: color 0.3s ease;
93}
94
95.mobileNav a:hover {
96 color: #3a86ff;
97}2. Cartes animées avec effet de retournement
1// components/FlipCard.tsx
2'use client';
3
4import { useState } from 'react';
5import { motion } from 'framer-motion';
6import styles from './FlipCard.module.css';
7
8interface FlipCardProps {
9 frontContent: React.ReactNode;
10 backContent: React.ReactNode;
11}
12
13export function FlipCard({ frontContent, backContent }: FlipCardProps) {
14 const [isFlipped, setIsFlipped] = useState(false);
15
16 const frontVariants = {
17 flipped: { rotateY: 180, opacity: 0 },
18 unflipped: { rotateY: 0, opacity: 1 }
19 };
20
21 const backVariants = {
22 flipped: { rotateY: 0, opacity: 1 },
23 unflipped: { rotateY: -180, opacity: 0 }
24 };
25
26 return (
27 <div
28 className={styles.flipCardContainer}
29 onClick={() => setIsFlipped(!isFlipped)}
30 >
31 <motion.div
32 className={styles.flipCardFront}
33 variants={frontVariants}
34 animate={isFlipped ? 'flipped' : 'unflipped'}
35 transition={{ duration: 0.6, type: 'spring', stiffness: 300, damping: 20 }}
36 >
37 {frontContent}
38 </motion.div>
39
40 <motion.div
41 className={styles.flipCardBack}
42 variants={backVariants}
43 animate={isFlipped ? 'flipped' : 'unflipped'}
44 transition={{ duration: 0.6, type: 'spring', stiffness: 300, damping: 20 }}
45 >
46 {backContent}
47 </motion.div>
48 </div>
49 );
50}1/* components/FlipCard.module.css */
2.flipCardContainer {
3 width: 300px;
4 height: 400px;
5 perspective: 1000px;
6 cursor: pointer;
7 position: relative;
8}
9
10.flipCardFront,
11.flipCardBack {
12 position: absolute;
13 width: 100%;
14 height: 100%;
15 backface-visibility: hidden;
16 border-radius: 16px;
17 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
18 display: flex;
19 flex-direction: column;
20 justify-content: center;
21 align-items: center;
22 padding: 20px;
23}
24
25.flipCardFront {
26 background-color: #f9f9f9;
27 color: #333;
28}
29
30.flipCardBack {
31 background-color: #3a86ff;
32 color: white;
33}3. Barre de progression de défilement
1// components/ScrollProgressBar.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import { motion, useScroll } from 'framer-motion';
6import styles from './ScrollProgressBar.module.css';
7
8export function ScrollProgressBar() {
9 const { scrollYProgress } = useScroll();
10
11 return (
12 <motion.div
13 className={styles.progressBar}
14 style={{ scaleX: scrollYProgress }}
15 />
16 );
17}1/* components/ScrollProgressBar.module.css */
2.progressBar {
3 position: fixed;
4 top: 0;
5 left: 0;
6 right: 0;
7 height: 5px;
8 background-color: #3a86ff;
9 transform-origin: 0%;
10 z-index: 1000;
11}Optimisation de la performance des animations
Les animations peuvent solliciter le navigateur, il vaut donc la peine d'appliquer des méthodes d'optimisation :
1. Utilise des propriétés CSS qui ne provoquent pas de reflow
Meilleures propriétés à animer :
transformopacityfilter
Évite d'animer :
width,heighttop,left,right,bottommargin,padding
2. Utilise will-change avec prudence
1.animated-element {
2 will-change: transform, opacity;
3 /* Rest of styles */
4}Remarque : abuser de
will-changepeut dégrader la performance.
3. Charger en lazy les bibliothèques d'animation lourdes
1// components/AnimatedSection.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import dynamic from 'next/dynamic';
6
7// Lazy loading Framer Motion
8const motion = dynamic(() => import('framer-motion').then((mod) => mod.motion), {
9 ssr: false,
10 loading: () => <div>Loading...</div>,
11});
12
13export function AnimatedSection({ children }) {
14 const [isClient, setIsClient] = useState(false);
15
16 useEffect(() => {
17 setIsClient(true);
18 }, []);
19
20 if (!isClient) {
21 return <div>{children}</div>;
22 }
23
24 return (
25 <motion.div
26 initial={{ opacity: 0 }}
27 animate={{ opacity: 1 }}
28 transition={{ duration: 0.5 }}
29 >
30 {children}
31 </motion.div>
32 );
33}4. Utiliser le GPU pour les animations
1.accelerated {
2 transform: translateZ(0); /* Enables GPU acceleration */
3}Accessibilité des animations
Souviens-toi que certains utilisateurs peuvent préférer des animations réduites, par ex. en raison de troubles vestibulaires.
1. Respecter les préférences système
1@media (prefers-reduced-motion: reduce) {
2 /* Disable or simplify animations */
3 * {
4 animation-duration: 0.01ms !important;
5 animation-iteration-count: 1 !important;
6 transition-duration: 0.01ms !important;
7 scroll-behavior: auto !important;
8 }
9}2. Implémenter une bascule d'animation
1// components/AnimationToggle.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import styles from './AnimationToggle.module.css';
6
7export function AnimationToggle() {
8 const [areAnimationsEnabled, setAreAnimationsEnabled] = useState(true);
9
10 useEffect(() => {
11 // Check system preferences
12 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
13 setAreAnimationsEnabled(!prefersReducedMotion);
14
15 // Check saved user preferences
16 const savedPreference = localStorage.getItem('animations-enabled');
17 if (savedPreference !== null) {
18 setAreAnimationsEnabled(savedPreference === 'true');
19 }
20 }, []);
21
22 const toggleAnimations = () => {
23 const newValue = !areAnimationsEnabled;
24 setAreAnimationsEnabled(newValue);
25 document.documentElement.classList.toggle('reduce-animations', !newValue);
26 localStorage.setItem('animations-enabled', String(newValue));
27 };
28
29 return (
30 <button
31 className={styles.toggleButton}
32 onClick={toggleAnimations}
33 aria-pressed={!areAnimationsEnabled}
34 >
35 {areAnimationsEnabled ? 'Disable animations' : 'Enable animations'}
36 </button>
37 );
38}1/* app/globals.css */
2.reduce-animations * {
3 animation-duration: 0.01ms !important;
4 animation-iteration-count: 1 !important;
5 transition-duration: 0.01ms !important;
6 scroll-behavior: auto !important;
7}Animations liées au défilement
1. Animation de base au défilement avec Intersection Observer
1// components/FadeInSection.tsx
2'use client';
3
4import { useRef, useEffect, useState } from 'react';
5import styles from './FadeInSection.module.css';
6
7export function FadeInSection({ children }) {
8 const [isVisible, setIsVisible] = useState(false);
9 const ref = useRef<HTMLDivElement>(null);
10
11 useEffect(() => {
12 const observer = new IntersectionObserver(
13 ([entry]) => {
14 setIsVisible(entry.isIntersecting);
15 },
16 {
17 root: null,
18 rootMargin: '0px',
19 threshold: 0.1,
20 }
21 );
22
23 if (ref.current) {
24 observer.observe(ref.current);
25 }
26
27 return () => {
28 if (ref.current) {
29 observer.unobserve(ref.current);
30 }
31 };
32 }, []);
33
34 return (
35 <div
36 ref={ref}
37 className={`${styles.fadeInSection} ${isVisible ? styles.isVisible : ''}`}
38 >
39 {children}
40 </div>
41 );
42}1/* components/FadeInSection.module.css */
2.fadeInSection {
3 opacity: 0;
4 transform: translateY(30px);
5 transition: opacity 1s ease, transform 1s ease;
6}
7
8.isVisible {
9 opacity: 1;
10 transform: translateY(0);
11}2. Animations de défilement avancées avec Framer Motion
1// components/ScrollAnimation.tsx
2'use client';
3
4import { useRef } from 'react';
5import { motion, useScroll, useTransform } from 'framer-motion';
6import styles from './ScrollAnimation.module.css';
7
8export function ScrollAnimation() {
9 const ref = useRef<HTMLDivElement>(null);
10
11 const { scrollYProgress } = useScroll({
12 target: ref,
13 offset: ["start end", "end start"]
14 });
15
16 const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
17 const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 0.8]);
18 const rotate = useTransform(scrollYProgress, [0, 0.5, 1], [-15, 0, 15]);
19
20 return (
21 <div ref={ref} className={styles.container}>
22 <motion.div
23 className={styles.animatedBox}
24 style={{ opacity, scale, rotate }}
25 >
26 <h2>Animation pendant le défilement</h2>
27 <p>This section animates based on scroll position.</p>
28 </motion.div>
29 </div>
30 );
31}1/* components/ScrollAnimation.module.css */
2.container {
3 height: 100vh;
4 display: flex;
5 justify-content: center;
6 align-items: center;
7}
8
9.animatedBox {
10 padding: 40px;
11 background-color: #3a86ff;
12 color: white;
13 border-radius: 16px;
14 max-width: 500px;
15 text-align: center;
16}Cas d'usage dans différents contextes
1. Tableaux de données animés
1// components/DataTable.tsx
2'use client';
3
4import { useState } from 'react';
5import { motion, AnimatePresence } from 'framer-motion';
6import styles from './DataTable.module.css';
7
8interface DataItem {
9 id: string;
10 name: string;
11 value: number;
12 category: string;
13}
14
15interface DataTableProps {
16 data: DataItem[];
17}
18
19export function DataTable({ data }: DataTableProps) {
20 const [sortField, setSortField] = useState<keyof DataItem>('name');
21 const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
22
23 const handleSort = (field: keyof DataItem) => {
24 if (field === sortField) {
25 setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
26 } else {
27 setSortField(field);
28 setSortDirection('asc');
29 }
30 };
31
32 const sortedData = [...data].sort((a, b) => {
33 if (a[sortField] < b[sortField]) return sortDirection === 'asc' ? -1 : 1;
34 if (a[sortField] > b[sortField]) return sortDirection === 'asc' ? 1 : -1;
35 return 0;
36 });
37
38 return (
39 <div className={styles.tableContainer}>
40 <table className={styles.table}>
41 <thead>
42 <tr>
43 <th onClick={() => handleSort('name')}>
44 Nom {sortField === 'name' && (sortDirection === 'asc' ? '↑' : '↓')}
45 </th>
46 <th onClick={() => handleSort('value')}>
47 Value {sortField === 'value' && (sortDirection === 'asc' ? '↑' : '↓')}
48 </th>
49 <th onClick={() => handleSort('category')}>
50 Kategoria {sortField === 'category' && (sortDirection === 'asc' ? '↑' : '↓')}
51 </th>
52 </tr>
53 </thead>
54 <motion.tbody layout>
55 <AnimatePresence>
56 {sortedData.map((item) => (
57 <motion.tr
58 key={item.id}
59 initial={{ opacity: 0, y: 20 }}
60 animate={{ opacity: 1, y: 0 }}
61 exit={{ opacity: 0, y: -20 }}
62 transition={{ duration: 0.3 }}
63 layout
64 >
65 <td>{item.name}</td>
66 <td>{item.value}</td>
67 <td>{item.category}</td>
68 </motion.tr>
69 ))}
70 </AnimatePresence>
71 </motion.tbody>
72 </table>
73 </div>
74 );
75}2. Interface de chat animée
1// components/ChatInterface.tsx
2'use client';
3
4import { useState } from 'react';
5import { motion, AnimatePresence } from 'framer-motion';
6import styles from './ChatInterface.module.css';
7
8interface Message {
9 id: number;
10 text: string;
11 sender: 'user' | 'assistant';
12 timestamp: Date;
13}
14
15export function ChatInterface() {
16 const [messages, setMessages] = useState<Message[]>([]);
17 const [inputText, setInputText] = useState('');
18
19 const handleSend = () => {
20 if (!inputText.trim()) return;
21
22 // Add user message
23 const userMessage: Message = {
24 id: Date.now(),
25 text: inputText,
26 sender: 'user',
27 timestamp: new Date(),
28 };
29
30 setMessages([...messages, userMessage]);
31 setInputText('');
32
33 // Simulate assistant response
34 setTimeout(() => {
35 const assistantMessage: Message = {
36 id: Date.now() + 1,
37 text: `I received your message: "${inputText}"`,
38 sender: 'assistant',
39 timestamp: new Date(),
40 };
41
42 setMessages((prev) => [...prev, assistantMessage]);
43 }, 1000);
44 };
45
46 return (
47 <div className={styles.chatContainer}>
48 <div className={styles.chatMessages}>
49 <AnimatePresence>
50 {messages.map((message) => (
51 <motion.div
52 key={message.id}
53 className={`${styles.message} ${styles[message.sender]}`}
54 initial={{ opacity: 0, y: 20, scale: 0.9 }}
55 animate={{ opacity: 1, y: 0, scale: 1 }}
56 transition={{ duration: 0.3 }}
57 >
58 <div className={styles.messageContent}>
59 {message.text}
60 </div>
61 <div className={styles.timestamp}>
62 {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
63 </div>
64 </motion.div>
65 ))}
66 </AnimatePresence>
67 </div>
68
69 <div className={styles.chatInput}>
70 <input
71 type="text"
72 value={inputText}
73 onChange={(e) => setInputText(e.target.value)}
74 onKeyPress={(e) => e.key === 'Enter' && handleSend()}
75 placeholder="Write a message..."
76 />
77 <button onClick={handleSend}>Send</button>
78 </div>
79 </div>
80 );
81}3. Sélection de thème animée
1// components/ThemeToggle.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import { motion } from 'framer-motion';
6import styles from './ThemeToggle.module.css';
7
8export function ThemeToggle() {
9 const [isDarkMode, setIsDarkMode] = useState(false);
10
11 useEffect(() => {
12 // Check system preferences or previously saved settings
13 const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
14 const savedTheme = localStorage.getItem('theme');
15 const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
16
17 setIsDarkMode(initialTheme === 'dark');
18 document.documentElement.classList.toggle('dark-theme', initialTheme === 'dark');
19 }, []);
20
21 const toggleTheme = () => {
22 const newMode = !isDarkMode;
23 setIsDarkMode(newMode);
24 document.documentElement.classList.toggle('dark-theme', newMode);
25 localStorage.setItem('theme', newMode ? 'dark' : 'light');
26 };
27
28 return (
29 <button
30 className={styles.themeToggle}
31 onClick={toggleTheme}
32 aria-label={isDarkMode ? 'Switch to light theme' : 'Switch to dark theme'}
33 >
34 <motion.div
35 className={styles.toggleTrack}
36 animate={{ backgroundColor: isDarkMode ? '#3a86ff' : '#ccc' }}
37 >
38 <motion.div
39 className={styles.toggleThumb}
40 animate={{
41 x: isDarkMode ? 22 : 0,
42 backgroundColor: isDarkMode ? '#121212' : '#fff',
43 }}
44 transition={{ type: 'spring', stiffness: 500, damping: 30 }}
45 >
46 <motion.div
47 className={styles.iconContainer}
48 animate={{ opacity: isDarkMode ? 1 : 0 }}
49 >
50 {/* Moon icon */}
51 ◐
52 </motion.div>
53 <motion.div
54 className={styles.iconContainer}
55 animate={{ opacity: isDarkMode ? 0 : 1 }}
56 >
57 {/* Sun icon */}
58 ●
59 </motion.div>
60 </motion.div>
61 </motion.div>
62 </button>
63 );
64}Bonnes pratiques pour créer des animations
- L'intentionnalité : chaque animation devrait avoir un but - informatif ou de navigation
- La subtilité : dans la plupart des cas, des animations subtiles valent mieux que des animations exagérées
- La cohérence : maintiens un langage d'animation cohérent dans toute l'application
- La performance : optimise les animations pour de meilleures performances
- L'accessibilité : respecte les préférences des utilisateurs concernant les animations réduites
- Le responsive : adapte les animations aux différentes tailles d'écran
- Les tests : teste les animations sur différents appareils et navigateurs
Résumé
Les animations et les transitions sont des outils puissants qui peuvent avoir un impact significatif sur l'expérience utilisateur de ton application Next.js 16. Tout comme à Metropolis Quantum, où les animations fluides des affichages holographiques et des systèmes de la ville rendent la technologie plus intuitive et conviviale, des animations bien conçues dans ton application améliorent l'utilisabilité, l'engagement et l'impression générale.
Points clés :
- Choisis les bons outils - du simple CSS aux bibliothèques avancées comme Framer Motion
- Pense à la performance - optimise les animations pour la fluidité et une faible consommation de ressources
- Préserve l'accessibilité - respecte les préférences des utilisateurs concernant les animations réduites
- Maintiens la cohérence - crée un langage d'animation cohérent avec l'identité de ton application
- L'objectif avant la décoration - chaque animation devrait remplir une fonction précise
Dans le prochain chapitre, nous étudierons les fonctionnalités de performance avancées de Next.js 16 qui permettront à ton application de fonctionner de façon fluide et rapide, tout comme les systèmes les plus avancés de Metropolis Quantum.
Code de cette leçon : app/page.tsx
1// Dark Mode & Theme System - Quantum Metropolis
2'use client';
3
4import { useState } from 'react';
5
6export default function DarkModeDemo() {
7 const [theme, setTheme] = useState<'dark' | 'light' | 'neon'>('dark');
8
9 const themes = {
10 dark: {
11 background: 'linear-gradient(135deg, #0f0f23, #1a1a2e)',
12 surface: 'rgba(255,255,255,0.05)',
13 surfaceBorder: 'rgba(100,255,218,0.2)',
14 text: '#ffffff',
15 textSecondary: '#b0bec5',
16 primary: '#64ffda',
17 secondary: '#7c4dff',
18 cardBg: 'rgba(15,15,35,0.9)'
19 },
20 light: {
21 background: 'linear-gradient(135deg, #f8f9fa, #e9ecef)',
22 surface: 'rgba(255,255,255,0.9)',
23 surfaceBorder: 'rgba(0,0,0,0.1)',
24 text: '#212529',
25 textSecondary: '#6c757d',
26 primary: '#0056cc',
27 secondary: '#4400cc',
28 cardBg: '#ffffff'
29 },
30 neon: {
31 background: 'linear-gradient(135deg, #000000, #1a0033)',
32 surface: 'rgba(255,0,128,0.1)',
33 surfaceBorder: 'rgba(255,0,128,0.5)',
34 text: '#ff0080',
35 textSecondary: '#ff69b4',
36 primary: '#ff0080',
37 secondary: '#00ff7f',
38 cardBg: 'rgba(0,0,0,0.8)'
39 }
40 };
41
42 const currentTheme = themes[theme];
43
44 return (
45 <div style={{
46 minHeight: '100vh',
47 background: currentTheme.background,
48 color: currentTheme.text,
49 padding: '3rem 2rem',
50 transition: 'all 0.5s ease'
51 }}>
52 {/* Theme Toggle */}
53 <div style={{textAlign: 'center', marginBottom: '3rem'}}>
54 <h1 style={{
55 fontSize: '3rem',
56 background: `linear-gradient(45deg, ${currentTheme.primary}, ${currentTheme.secondary})`,
57 WebkitBackgroundClip: 'text',
58 WebkitTextFillColor: 'transparent',
59 marginBottom: '2rem'
60 }}>
61 Dark Mode & Theming
62 </h1>
63
64 <div style={{display: 'flex', gap: '1rem', justifyContent: 'center', flexWrap: 'wrap'}}>
65 <button
66 onClick={() => setTheme('dark')}
67 style={{
68 padding: '0.75rem 2rem',
69 borderRadius: '0.75rem',
70 border: `2px solid ${theme === 'dark' ? currentTheme.primary : currentTheme.surfaceBorder}`,
71 background: theme === 'dark' ? currentTheme.primary : 'transparent',
72 color: theme === 'dark' ? '#0f0f23' : currentTheme.text,
73 fontWeight: 600,
74 cursor: 'pointer',
75 transition: 'all 0.3s ease'
76 }}
77 >
78 Dark
79 </button>
80
81 <button
82 onClick={() => setTheme('light')}
83 style={{
84 padding: '0.75rem 2rem',
85 borderRadius: '0.75rem',
86 border: `2px solid ${theme === 'light' ? currentTheme.primary : currentTheme.surfaceBorder}`,
87 background: theme === 'light' ? currentTheme.primary : 'transparent',
88 color: theme === 'light' ? '#ffffff' : currentTheme.text,
89 fontWeight: 600,
90 cursor: 'pointer',
91 transition: 'all 0.3s ease'
92 }}
93 >
94 Light
95 </button>
96
97 <button
98 onClick={() => setTheme('neon')}
99 style={{
100 padding: '0.75rem 2rem',
101 borderRadius: '0.75rem',
102 border: `2px solid ${theme === 'neon' ? currentTheme.primary : currentTheme.surfaceBorder}`,
103 background: theme === 'neon' ? currentTheme.primary : 'transparent',
104 color: theme === 'neon' ? '#000000' : currentTheme.text,
105 fontWeight: 600,
106 cursor: 'pointer',
107 transition: 'all 0.3s ease'
108 }}
109 >
110 Neon
111 </button>
112 </div>
113 </div>
114
115 {/* CSS Variables Example */}
116 <section style={{maxWidth: '1200px', margin: '0 auto 3rem', background: currentTheme.surface, padding: '2rem', borderRadius: '1rem', border: `1px solid ${currentTheme.surfaceBorder}`, transition: 'all 0.5s ease'}}>
117 <h2 style={{color: currentTheme.primary, marginBottom: '1.5rem'}}>CSS Custom Properties</h2>
118 <pre style={{background: 'rgba(0,0,0,0.3)', padding: '1.5rem', borderRadius: '0.75rem', overflow: 'auto', color: currentTheme.textSecondary, fontSize: '0.875rem'}}>
119{`:root {
120 --color-bg: ${currentTheme.background};
121 --color-surface: ${currentTheme.surface};
122 --color-text: ${currentTheme.text};
123 --color-primary: ${currentTheme.primary};
124 --color-secondary: ${currentTheme.secondary};
125}
126
127[data-theme="dark"] {
128 --color-bg: #0f0f23;
129 --color-text: #ffffff;
130}
131
132[data-theme="light"] {
133 --color-bg: #ffffff;
134 --color-text: #212529;
135}`}
136 </pre>
137 </section>
138
139 {/* Component Examples */}
140 <section style={{maxWidth: '1200px', margin: '0 auto 3rem'}}>
141 <h2 style={{color: currentTheme.primary, marginBottom: '2rem', textAlign: 'center'}}>Themed Components</h2>
142
143 <div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '2rem'}}>
144 <div style={{background: currentTheme.cardBg, padding: '2rem', borderRadius: '1rem', border: `1px solid ${currentTheme.surfaceBorder}`, transition: 'all 0.5s ease'}}>
145 <div style={{fontSize: '3rem', marginBottom: '1rem', textAlign: 'center'}}>↯</div>
146 <h3 style={{color: currentTheme.primary, marginBottom: '0.75rem', textAlign: 'center', fontSize: '1.5rem'}}>Energy System</h3>
147 <p style={{color: currentTheme.textSecondary, lineHeight: 1.6, textAlign: 'center'}}>
148 Quantum energy management in the metropolis
149 </p>
150 <div style={{marginTop: '1.5rem', width: '100%', height: '8px', background: currentTheme.surface, borderRadius: '4px', overflow: 'hidden'}}>
151 <div style={{width: '78%', height: '100%', background: `linear-gradient(90deg, ${currentTheme.primary}, ${currentTheme.secondary})`, borderRadius: '4px'}}></div>
152 </div>
153 </div>
154
155 <div style={{background: currentTheme.cardBg, padding: '2rem', borderRadius: '1rem', border: `1px solid ${currentTheme.surfaceBorder}`, transition: 'all 0.5s ease'}}>
156 <div style={{fontSize: '3rem', marginBottom: '1rem', textAlign: 'center'}}>◆</div>
157 <h3 style={{color: currentTheme.primary, marginBottom: '0.75rem', textAlign: 'center', fontSize: '1.5rem'}}>AI Core</h3>
158 <p style={{color: currentTheme.textSecondary, lineHeight: 1.6, textAlign: 'center'}}>
159 Intelligence artificielle de nouvelle génération
160 </p>
161 <div style={{marginTop: '1.5rem', width: '100%', height: '8px', background: currentTheme.surface, borderRadius: '4px', overflow: 'hidden'}}>
162 <div style={{width: '92%', height: '100%', background: `linear-gradient(90deg, ${currentTheme.primary}, ${currentTheme.secondary})`, borderRadius: '4px'}}></div>
163 </div>
164 </div>
165
166 <div style={{background: currentTheme.cardBg, padding: '2rem', borderRadius: '1rem', border: `1px solid ${currentTheme.surfaceBorder}`, transition: 'all 0.5s ease'}}>
167 <div style={{fontSize: '3rem', marginBottom: '1rem', textAlign: 'center'}}>◆</div>
168 <h3 style={{color: currentTheme.primary, marginBottom: '0.75rem', textAlign: 'center', fontSize: '1.5rem'}}>Network</h3>
169 <p style={{color: currentTheme.textSecondary, lineHeight: 1.6, textAlign: 'center'}}>
170 Global quantum network
171 </p>
172 <div style={{marginTop: '1.5rem', width: '100%', height: '8px', background: currentTheme.surface, borderRadius: '4px', overflow: 'hidden'}}>
173 <div style={{width: '65%', height: '100%', background: `linear-gradient(90deg, ${currentTheme.primary}, ${currentTheme.secondary})`, borderRadius: '4px'}}></div>
174 </div>
175 </div>
176 </div>
177 </section>
178
179 {/* System Preference Detection */}
180 <section style={{maxWidth: '1200px', margin: '0 auto 3rem', background: currentTheme.surface, padding: '2rem', borderRadius: '1rem', border: `1px solid ${currentTheme.surfaceBorder}`, transition: 'all 0.5s ease'}}>
181 <h2 style={{color: currentTheme.primary, marginBottom: '1.5rem'}}>System Preference Detection</h2>
182 <pre style={{background: 'rgba(0,0,0,0.3)', padding: '1.5rem', borderRadius: '0.75rem', overflow: 'auto', color: currentTheme.textSecondary, fontSize: '0.875rem'}}>
183{`@media (prefers-color-scheme: dark) {
184 :root {
185 --color-bg: #0f0f23;
186 --color-text: #ffffff;
187 }
188}
189
190@media (prefers-color-scheme: light) {
191 :root {
192 --color-bg: #ffffff;
193 --color-text: #212529;
194 }
195}`}
196 </pre>
197 </section>
198 </div>
199 );
200}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelles propriétés CSS sont les plus performantes à animer (elles ne déclenchent pas de reflow), comme montré dans la leçon ?
2. À quoi sert la règle CSS @keyframes ?
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne la syntaxe de déclaration d’une transition CSS (sur une seule ligne) :