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-group1// components/Notification.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5import { CSSTransition } from 'react-transition-group';
6import styles from './Notification.module.css';
7
8interface NotificationProps {
9 message: string;
10 type?: 'success' | 'error' | 'info';
11 duration?: number;
12}
13
14export function Notification({ message, type = 'info', duration = 3000 }: NotificationProps) {
15 const [show, setShow] = useState(true);
16
17 useEffect(() => {
18 const timer = setTimeout(() => {
19 setShow(false);
20 }, duration);
21
22 return () => clearTimeout(timer);
23 }, [duration]);
24
25 return (
26 <CSSTransition
27 in={show}
28 timeout={300}
29 classNames={{
30 enter: styles.notificationEnter,
31 enterActive: styles.notificationEnterActive,
32 exit: styles.notificationExit,
33 exitActive: styles.notificationExitActive,
34 }}
35 unmountOnExit
36 >
37 <div className={`${styles.notification} ${styles[type]}`}>
38 {message}
39 </div>
40 </CSSTransition>
41 );
42}1/* components/Notification.module.css */
2.notification {
3 position: fixed;
4 bottom: 20px;
5 right: 20px;
6 padding: 16px;
7 border-radius: 8px;
8 color: white;
9 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
10 z-index: 1000;
11}
12
13.success {
14 background-color: #10b981;
15}
16
17.error {
18 background-color: #ef4444;
19}
20
21.info {
22 background-color: #3b82f6;
23}
24
25.notificationEnter {
26 opacity: 0;
27 transform: translateY(20px);
28}
29
30.notificationEnterActive {
31 opacity: 1;
32 transform: translateY(0);
33 transition: opacity 300ms, transform 300ms;
34}
35
36.notificationExit {
37 opacity: 1;
38}
39
40.notificationExitActive {
41 opacity: 0;
42 transform: translateY(20px);
43 transition: opacity 300ms, transform 300ms;
44}5. Framer Motion
Framer Motion is a powerful animation library for React that simplifies creating complex animations:
1npm install framer-motion1// components/Card.tsx
2'use client';
3
4import { motion } from 'framer-motion';
5import styles from './Card.module.css';
6
7interface CardProps {
8 title: string;
9 description: string;
10 imageUrl: string;
11 index: number;
12}
13
14export function Card({ title, description, imageUrl, index }: CardProps) {
15 return (
16 <motion.div
17 className={styles.card}
18 initial={{ opacity: 0, y: 20 }}
19 animate={{ opacity: 1, y: 0 }}
20 transition={{
21 duration: 0.5,
22 delay: index * 0.1,
23 ease: "easeOut"
24 }}
25 whileHover={{
26 scale: 1.05,
27 boxShadow: "0 10px 30px rgba(0, 0, 0, 0.15)"
28 }}
29 >
30 <div className={styles.imageContainer}>
31 <img src={imageUrl} alt={title} />
32 </div>
33 <div className={styles.content}>
34 <h3>{title}</h3>
35 <p>{description}</p>
36 </div>
37 </motion.div>
38 );
39}6. CSS Modules 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:
transformopacityfilter
Unikaj animowania:
width,heighttop,left,right,bottommargin,padding
2. Use will-change Carefully
1.animated-element {
2 will-change: transform, opacity;
3 /* Rest of styles */
4}Note: Overusing
will-changecan 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
- Purposefulness: Every animation should have a purpose - whether informational or navigational
- Subtlety: In most cases, subtle animations are better than exaggerated ones
- Consistency: Maintain a consistent animation language throughout the application
- Performance: Optimize animations for better performance
- Accessibility: Respect user preferences regarding reduced animations
- Responsiveness: Adapt animations to different screen sizes
- 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:
- Choose the right tools - from simple CSS to advanced libraries like Framer Motion
- Remember performance - optimize animations for smoothness and low resource consumption
- Maintain accessibility - respect user preferences regarding reduced animations
- Maintain consistency - create an animation language consistent with your application's identity
- 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. Which CSS properties are the most performant to animate (do not trigger reflow), as shown in the lesson?
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):