CodeWorlds

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

Next.js course · Module 3: Styling and Visual Optimization

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

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 : ''}`}>
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

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 16

Next.js 16 uses React 19.2, so besides classic techniques such as CSS animations or Framer Motion, you can use React's <ViewTransition> component. In the App Router it works without any configuration, because every navigation is a transition, and browsers without View Transitions API support simply show the new page without animation.

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 16 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 16 that will allow your application to run smoothly and quickly, just like the most advanced systems in Metropolis Quantum.

Code for this lesson: 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              Next-generation artificial intelligence
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}

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which CSS properties are the most performant to animate (do not trigger reflow), as shown in the lesson?

  2. 2. What is the @keyframes CSS rule used for?

Hands-on tasks in the game

  • Horizontal ordering

    Arrange the CSS transition declaration syntax (single line):

Useful articles