We use cookies to enhance your experience on the site
CodeWorlds

Animations and Transitions in the User Interface in Next.js 15

In Metropolis Quantum, nothing stands still. Vehicles move smoothly between districts, holographic displays change with elegant transitions, and interactive city elements respond to residents with subtle but meaningful animations. This dynamism not only provides visual appeal but above all improves the usability and intuitiveness of the city's systems.

Similarly in web applications - well-designed animations and transitions can significantly impact user experience, making the interface more natural, informative, and pleasant to use. In this chapter, we will learn different techniques for implementing animations and transitions in Next.js 15 applications.

Types of Animations in Web Interfaces

Before we move on to specific implementations, it is worth knowing the different categories of animations found in web interfaces:

1. Animacje mikro-interakcji

Subtle animations responding to user actions, e.g.:

  • Efekty najechania na przyciski
  • Click animations
  • Loading indicators
  • Form state changes

2. State Transition Animations

Smooth transitions between different component states:

  • Rozwijane menu
  • Enlarging/shrinking elements
  • Element visibility changes
  • Mode switching (light/dark)

3. Animacje nawigacyjne

Animations accompanying page or view changes:

  • Page transitions
  • Tab switching
  • Przewijanie strony
  • Efekty paralaksy

4. Animacje narracyjne

More complex animations presenting a story or process:

  • Sekwencje onboardingowe
  • Process illustrations
  • 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

The simplest and most efficient way to animate simple changes:

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

For more complex animations with defined cycles:

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

For animations dependent on React states:

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; /* 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

A library that simplifies managing element transitions in 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 is a powerful animation library for React that simplifies creating complex animations:

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 allows easy combination of CSS Modules with components:

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}

Page Transition Animations in Next.js 15

Next.js 15 introduces an improved routing system that allows implementation of smooth page transitions.

1. Basic Transitions Using 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. Advanced Transitions with 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      {/* Rest of content */}
9    </PageTransition>
10  );
11}

3. Transitions Using 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}

Implementing Complex UI Animations with Framer Motion

Framer Motion is particularly useful for creating complex, interactive UI animations. Let's look at a few examples:

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. Scroll Progress Bar

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}

Animation Performance Optimization

Animations can be taxing on the browser, so it is worth applying optimization methods:

1. Use CSS Properties That Do Not Cause Reflow

Best properties to animate:

  • transform
  • opacity
  • filter

Unikaj animowania:

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

2. Use
will-change
Carefully

1.animated-element {
2  will-change: transform, opacity;
3  /* Rest of styles */
4}

Note: Overusing

will-change
can worsen performance.

3. Lazy Loading Heavy Animation Libraries

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. Using GPU for Animations

1.accelerated {
2  transform: translateZ(0); /* Enables GPU acceleration */
3}

Animation Accessibility

Remember that some users may prefer reduced animations, e.g., due to vestibular disorders.

1. Respektowanie preferencji systemowych

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. Implementing an Animation Toggle

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}

Scroll-dependent Animations

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

Use Cases in Different Contexts

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              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. 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    // 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. Animated Theme Selection

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}

Best Practices przy tworzeniu animacji

  1. Purposefulness: Every animation should have a purpose - whether informational or navigational
  2. Subtlety: In most cases, subtle animations are better than exaggerated ones
  3. Consistency: Maintain a consistent animation language throughout the application
  4. Performance: Optimize animations for better performance
  5. Accessibility: Respect user preferences regarding reduced animations
  6. Responsiveness: Adapt animations to different screen sizes
  7. Testing: Test animations on different devices and browsers

Summary

Animations and transitions are powerful tools that can significantly impact your Next.js 15 application's user experience. Just like in Metropolis Quantum, where smooth animations of holographic displays and city systems make technology more intuitive and friendly, well-designed animations in your application improve usability, engagement, and overall impressions.

Key points:

  1. Choose the right tools - from simple CSS to advanced libraries like Framer Motion
  2. Remember performance - optimize animations for smoothness and low resource consumption
  3. Maintain accessibility - respect user preferences regarding reduced animations
  4. Maintain consistency - create an animation language consistent with your application's identity
  5. Purpose over decoration - every animation should serve a specific function

In the next chapter, we will look at advanced performance features of Next.js 15 that will allow your application to run smoothly and quickly, just like the most advanced systems in Metropolis Quantum.

Go to CodeWorlds