Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Animacje i przejścia w interfejsie użytkownika w Next.js 15

W Metropolis Quantum nic nie stoi w miejscu. Pojazdy przemieszczają się płynnie między dzielnicami, holograficzne wyświetlacze zmieniają się z eleganckimi przejściami, a interaktywne elementy miasta reagują na mieszkańców subtelnymi, ale znaczącymi animacjami. Ta dynamika nie tylko zapewnia atrakcyjność wizualną, ale przede wszystkim poprawia użyteczność i intuicyjność miejskich systemów.

Podobnie w aplikacjach webowych - dobrze zaprojektowane animacje i przejścia mogą znacząco wpłynąć na doświadczenie użytkownika, czyniąc interfejs bardziej naturalnym, informacyjnym i przyjemnym w obsłudze. W tym rozdziale poznamy różne techniki implementacji animacji i przejść w aplikacjach Next.js 15.

Rodzaje animacji w interfejsach webowych

Zanim przejdziemy do konkretnych implementacji, warto poznać różne kategorie animacji występujących w interfejsach webowych:

1. Animacje mikro-interakcji

Subtelne animacje reagujące na działania użytkownika, np.:

  • Efekty najechania na przyciski
  • Animacje kliknięcia
  • Wskaźniki ładowania
  • Zmiany stanów formularzy

2. Animacje przejść między stanami

Płynne przejścia między różnymi stanami komponentów:

  • Rozwijane menu
  • Powiększanie/zmniejszanie elementów
  • Zmiany widoczności elementów
  • Przełączanie trybów (jasny/ciemny)

3. Animacje nawigacyjne

Animacje towarzyszące zmianie stron lub widoków:

  • Przejścia między stronami
  • Zmiana zakładek
  • Przewijanie strony
  • Efekty paralaksy

4. Animacje narracyjne

Bardziej złożone animacje przedstawiające historię lub proces:

  • Sekwencje onboardingowe
  • Ilustracje procesów
  • Animowane infografiki
  • Storytelling wizualny

Implementacja animacji w React i Next.js 15

W ekosystemie React i Next.js mamy do dyspozycji wiele metod implementacji animacji:

1. CSS i CSS transitions

Najprostszy i najbardziej wydajny sposób animowania prostych zmian:

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. CSS Animations

Dla bardziej złożonych animacji, które mają określone cykle:

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/* Sekwencyjne animacje dla listy elementów */
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 i useState

Dla animacji zależnych od stanów 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 : ''}`}>
1819        </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; /* Warto dostosować do potrzeb */
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

Biblioteka ułatwiająca zarządzanie przejściami elementów w React:

1npm install react-transition-group
1// 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 to potężna biblioteka animacji dla React, która upraszcza tworzenie złożonych animacji:

1npm install framer-motion
1// 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 z Next.js

Next.js pozwala na łatwe łączenie CSS Modules z komponentami:

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}

Animacje przejść między stronami w Next.js 15

Next.js 15 wprowadza ulepszony system routingu, który pozwala na implementację płynnych przejść między stronami.

1. Podstawowe przejścia z użyciem 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. Zaawansowane przejścia z 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>Witaj w Metropolis Quantum</h1>
8      {/* Reszta zawartości */}
9    </PageTransition>
10  );
11}

3. Przejścia z użyciem 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); // Daj czas na animację wyjścia
18  };
19  
20  return (
21    <nav className={`${styles.nav} ${isNavigating ? styles.navigating : ''}`}>
22      <ul>
23        <li>
24          <a onClick={() => handleNavigate('/')}>Strona główna</a>
25        </li>
26        <li>
27          <a onClick={() => handleNavigate('/about')}>O nas</a>
28        </li>
29        <li>
30          <a onClick={() => handleNavigate('/services')}>Usługi</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}

Implementacja złożonych animacji UI z Framer Motion

Framer Motion jest szczególnie przydatny do tworzenia złożonych, interaktywnych animacji UI. Przyjrzyjmy się kilku przykładom:

1. Animowane menu mobilne

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. Animowane karty z efektem przerzucania

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. Pasek postępu przy przewijaniu strony

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}

Optymalizacja wydajności animacji

Animacje mogą być obciążające dla przeglądarki, dlatego warto stosować metody optymalizacji:

1. Korzystaj z właściwości CSS, które nie powodują reflow

Najlepiej animować:

  • transform
  • opacity
  • filter

Unikaj animowania:

  • width
    ,
    height
  • top
    ,
    left
    ,
    right
    ,
    bottom
  • margin
    ,
    padding

2. Używaj
will-change
z rozwagą

1.animated-element {
2  will-change: transform, opacity;
3  /* Reszta stylów */
4}

Uwaga: Nadużywanie

will-change
może pogorszyć wydajność.

3. Opóźnione ładowanie ciężkich bibliotek animacji

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. Używanie GPU do animacji

1.accelerated {
2  transform: translateZ(0); /* Włącza akcelerację GPU */
3}

Dostępność animacji (accessibility)

Pamiętaj, że niektórzy użytkownicy mogą preferować zredukowane animacje, np. ze względu na zaburzenia przedsionkowe.

1. Respektowanie preferencji systemowych

1@media (prefers-reduced-motion: reduce) {
2  /* Wyłącz lub uprość animacje */
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. Implementacja przełącznika animacji

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    // Sprawdź preferencje systemowe
12    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
13    setAreAnimationsEnabled(!prefersReducedMotion);
14    
15    // Sprawdź zapisane preferencje użytkownika
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 ? 'Wyłącz animacje' : 'Włącz animacje'}
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}

Animacje zależne od scroll-a

1. Podstawowa animacja podczas przewijania z 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. Zaawansowane animacje scroll z 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>Animacja podczas przewijania</h2>
27        <p>Ta sekcja animuje się w zależności od pozycji przewijania.</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}

Przypadki użycia w różnych kontekstach

1. Animowane tabele danych

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              Nazwa {sortField === 'name' && (sortDirection === 'asc' ? '↑' : '↓')}
45            </th>
46            <th onClick={() => handleSort('value')}>
47              Wartość {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. Animowany interfejs czatu

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    // Dodaj wiadomość użytkownika
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    // Symuluj odpowiedź asystenta
34    setTimeout(() => {
35      const assistantMessage: Message = {
36        id: Date.now() + 1,
37        text: `Otrzymałem Twoją wiadomość: "${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="Napisz wiadomość..."
76        />
77        <button onClick={handleSend}>Wyślij</button>
78      </div>
79    </div>
80  );
81}

3. Animowany wybór motywu

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    // Sprawdź preferencje systemowe lub zapisane wcześniej ustawienia
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 ? 'Przełącz na jasny motyw' : 'Przełącz na ciemny motyw'}
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            {/* Ikona księżyca */}
51            🌙
52          </motion.div>
53          <motion.div 
54            className={styles.iconContainer}
55            animate={{ opacity: isDarkMode ? 0 : 1 }}
56          >
57            {/* Ikona słońca */}
58            ☀️
59          </motion.div>
60        </motion.div>
61      </motion.div>
62    </button>
63  );
64}

Najlepsze praktyki przy tworzeniu animacji

  1. Celowość: Każda animacja powinna mieć cel - czy to informacyjny, czy nawigacyjny
  2. Subtelność: W większości przypadków, subtelne animacje są lepsze niż przesadne
  3. Spójność: Utrzymuj spójny język animacji w całej aplikacji
  4. Wydajność: Optymalizuj animacje dla lepszej wydajności
  5. Dostępność: Respektuj preferencje użytkowników dotyczące zredukowanych animacji
  6. Responsywność: Dostosowuj animacje do różnych rozmiarów ekranów
  7. Testowanie: Testuj animacje na różnych urządzeniach i przeglądarkach

Podsumowanie

Animacje i przejścia to potężne narzędzia, które mogą znacząco wpłynąć na doświadczenia użytkowników Twojej aplikacji Next.js 15. Podobnie jak w Metropolis Quantum, gdzie płynne animacje holograficznych wyświetlaczy i systemów miejskich sprawiają, że technologia staje się bardziej intuicyjna i przyjazna, dobrze zaprojektowane animacje w Twojej aplikacji poprawiają użyteczność, zaangażowanie i ogólne wrażenia.

Kluczowe punkty:

  1. Wybieraj odpowiednie narzędzia - od prostego CSS po zaawansowane biblioteki jak Framer Motion
  2. Pamiętaj o wydajności - optymalizuj animacje dla płynności i niskiego zużycia zasobów
  3. Zachowaj dostępność - szanuj preferencje użytkowników dotyczące zredukowanych animacji
  4. Utrzymuj spójność - stwórz język animacji zgodny z tożsamością Twojej aplikacji
  5. Celowość ponad ozdobność - każda animacja powinna służyć konkretnej funkcji

W następnym rozdziale przyjrzymy się zaawansowanym funkcjom wydajnościowym Next.js 15, które pozwolą Twojej aplikacji działać płynnie i szybko, podobnie jak najbardziej zaawansowane systemy w Metropolis Quantum.

Przejdź do CodeWorlds