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.
Before we move on to specific implementations, it is worth knowing the different categories of animations found in web interfaces:
Subtle animations responding to user actions, e.g.:
Smooth transitions between different component states:
Animations accompanying page or view changes:
More complex animations presenting a story or process:
W ekosystemie React i Next.js mamy do dyspozycji wiele metod implementacji animacji:
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}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; }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}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}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}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}Next.js 15 introduces an improved routing system that allows implementation of smooth page transitions.
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}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}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}Framer Motion is particularly useful for creating complex, interactive UI animations. Let's look at a few examples:
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}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}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}Animations can be taxing on the browser, so it is worth applying optimization methods:
Best properties to animate:
transformopacityfilterUnikaj animowania:
width, heighttop, left, right, bottommargin, paddingwill-change Carefully1.animated-element {
2 will-change: transform, opacity;
3 /* Rest of styles */
4}Note: Overusing
can worsen performance.will-change
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}1.accelerated {
2 transform: translateZ(0); /* Enables GPU acceleration */
3}Remember that some users may prefer reduced animations, e.g., due to vestibular disorders.
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}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}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}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}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}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}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}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:
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.