Next.js course · Module 3: Styling and Visual Optimization
Animations and Transitions in the User Interface in Next.js 16
In this lesson10
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. Micro-interaction Animations
Subtle animations responding to user actions, e.g.:
- Button hover effects
- Click animations
- Loading indicators
- Form state changes
2. State Transition Animations
Smooth transitions between different component states:
- Expanding menus
- Enlarging/shrinking elements
- Element visibility changes
- Mode switching (light/dark)
3. Navigation Animations
Animations accompanying page or view changes:
- Page transitions
- Tab switching
- Page scrolling
- Parallax effects
4. Narrative Animations
More complex animations presenting a story or process:
- Onboarding sequences
- Process illustrations
- Animated infographics
- Visual storytelling
Implementing Animations in React and Next.js 16
The React and Next.js ecosystem offers many ways to implement animations:
1. CSS and CSS Transitions
The simplest and most efficient way to animate simple changes:
/* app/globals.css */
.button {
background-color: #3a86ff;
color: white;
padding: 8px 16px;
border-radius: 4px;
transition: transform 0.2s ease, background-color 0.2s ease;
}
.button:hover {
transform: translateY(-2px);
background-color: #2563eb;
}2. CSS Animations
For more complex animations with defined cycles:
/* app/globals.css */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fadeIn 0.5s ease forwards;
}
/* Sequential animations for a list of elements */
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }3. React Hooks and useState
For animations dependent on React states:
// components/ExpandablePanel.tsx
'use client';
import { useState } from 'react';
import styles from './ExpandablePanel.module.css';
export function ExpandablePanel({ title, children }) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className={styles.panel}>
<button
className={styles.header}
onClick={() => setIsExpanded(!isExpanded)}
>
<h3>{title}</h3>
<span className={`${styles.icon} ${isExpanded ? styles.iconExpanded : ''}`}>
▼
</span>
</button>
<div className={`
${styles.content}
${isExpanded ? styles.contentExpanded : styles.contentCollapsed}
`}>
{children}
</div>
</div>
);
}/* components/ExpandablePanel.module.css */
.panel {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
margin-bottom: 16px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background-color: #f9f9f9;
cursor: pointer;
width: 100%;
text-align: left;
border: none;
}
.icon {
transition: transform 0.3s ease;
}
.iconExpanded {
transform: rotate(180deg);
}
.content {
padding: 0 16px;
transition: all 0.3s ease;
overflow: hidden;
}
.contentExpanded {
max-height: 500px; /* Adjust to your needs */
padding: 16px;
opacity: 1;
}
.contentCollapsed {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
opacity: 0;
}4. React Transition Group
A library that simplifies managing element transitions in React:
npm install react-transition-group// components/Notification.tsx
'use client';
import { useState, useEffect } from 'react';
import { CSSTransition } from 'react-transition-group';
import styles from './Notification.module.css';
interface NotificationProps {
message: string;
type?: 'success' | 'error' | 'info';
duration?: number;
}
export function Notification({ message, type = 'info', duration = 3000 }: NotificationProps) {
const [show, setShow] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setShow(false);
}, duration);
return () => clearTimeout(timer);
}, [duration]);
return (
<CSSTransition
in={show}
timeout={300}
classNames={{
enter: styles.notificationEnter,
enterActive: styles.notificationEnterActive,
exit: styles.notificationExit,
exitActive: styles.notificationExitActive,
}}
unmountOnExit
>
<div className={`${styles.notification} ${styles[type]}`}>
{message}
</div>
</CSSTransition>
);
}/* components/Notification.module.css */
.notification {
position: fixed;
bottom: 20px;
right: 20px;
padding: 16px;
border-radius: 8px;
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
.success {
background-color: #10b981;
}
.error {
background-color: #ef4444;
}
.info {
background-color: #3b82f6;
}
.notificationEnter {
opacity: 0;
transform: translateY(20px);
}
.notificationEnterActive {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms, transform 300ms;
}
.notificationExit {
opacity: 1;
}
.notificationExitActive {
opacity: 0;
transform: translateY(20px);
transition: opacity 300ms, transform 300ms;
}5. Framer Motion
Framer Motion is a powerful animation library for React that simplifies creating complex animations:
npm install framer-motion// components/Card.tsx
'use client';
import { motion } from 'framer-motion';
import styles from './Card.module.css';
interface CardProps {
title: string;
description: string;
imageUrl: string;
index: number;
}
export function Card({ title, description, imageUrl, index }: CardProps) {
return (
<motion.div
className={styles.card}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.5,
delay: index * 0.1,
ease: "easeOut"
}}
whileHover={{
scale: 1.05,
boxShadow: "0 10px 30px rgba(0, 0, 0, 0.15)"
}}
>
<div className={styles.imageContainer}>
<img src={imageUrl} alt={title} />
</div>
<div className={styles.content}>
<h3>{title}</h3>
<p>{description}</p>
</div>
</motion.div>
);
}6. CSS Modules with Next.js
Next.js allows easy combination of CSS Modules with components:
// components/Button.tsx
'use client';
import { useState } from 'react';
import styles from './Button.module.css';
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
}
export function Button({ children, onClick }: ButtonProps) {
const [isPressed, setIsPressed] = useState(false);
const handleClick = () => {
setIsPressed(true);
setTimeout(() => setIsPressed(false), 200);
onClick && onClick();
};
return (
<button
className={`${styles.button} ${isPressed ? styles.pressed : ''}`}
onClick={handleClick}
>
{children}
</button>
);
}/* components/Button.module.css */
.button {
background-color: #3a86ff;
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.button:hover {
background-color: #2563eb;
}
.button:before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.4s ease-out, height 0.4s ease-out;
}
.button:hover:before {
width: 300px;
height: 300px;
}
.pressed {
transform: scale(0.98);
}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
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pl">
<body>
<main className="page-transition">
{children}
</main>
</body>
</html>
);
}/* app/globals.css */
.page-transition {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}2. Advanced Transitions with Framer Motion
// components/PageTransition.tsx
'use client';
import { motion } from 'framer-motion';
const variants = {
hidden: { opacity: 0, x: -200, y: 0 },
enter: { opacity: 1, x: 0, y: 0 },
exit: { opacity: 0, x: 0, y: -100 },
};
export function PageTransition({ children }: { children: React.ReactNode }) {
return (
<motion.div
variants={variants}
initial="hidden"
animate="enter"
exit="exit"
transition={{ type: 'linear', duration: 0.5 }}
>
{children}
</motion.div>
);
}// app/page.tsx
import { PageTransition } from '@/components/PageTransition';
export default function HomePage() {
return (
<PageTransition>
<h1>Welcome to Metropolis Quantum</h1>
{/* Rest of content */}
</PageTransition>
);
}3. Transitions Using next/navigation
// components/Navigation.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import styles from './Navigation.module.css';
export function Navigation() {
const router = useRouter();
const [isNavigating, setIsNavigating] = useState(false);
const handleNavigate = (href: string) => {
setIsNavigating(true);
setTimeout(() => {
router.push(href);
}, 500); // Give time for exit animation
};
return (
<nav className={`${styles.nav} ${isNavigating ? styles.navigating : ''}`}>
<ul>
<li>
<a onClick={() => handleNavigate('/')}>Home</a>
</li>
<li>
<a onClick={() => handleNavigate('/about')}>About us</a>
</li>
<li>
<a onClick={() => handleNavigate('/services')}>Services</a>
</li>
<li>
<a onClick={() => handleNavigate('/contact')}>Contact</a>
</li>
</ul>
</nav>
);
}/* components/Navigation.module.css */
.nav {
transition: opacity 0.5s ease;
}
.navigating {
opacity: 0.5;
pointer-events: none;
}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. Animated Mobile Menu
// components/MobileMenu.tsx
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import styles from './MobileMenu.module.css';
export function MobileMenu() {
const [isOpen, setIsOpen] = useState(false);
const menuVariants = {
closed: {
opacity: 0,
x: '100%',
transition: {
type: 'spring',
stiffness: 400,
damping: 40,
},
},
open: {
opacity: 1,
x: 0,
transition: {
type: 'spring',
stiffness: 400,
damping: 40,
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const menuItemVariants = {
closed: { opacity: 0, x: 50 },
open: { opacity: 1, x: 0 },
};
const buttonVariants = {
closed: { rotate: 0 },
open: { rotate: 90 },
};
return (
<div className={styles.mobileMenuContainer}>
<motion.button
className={styles.menuButton}
onClick={() => setIsOpen(!isOpen)}
variants={buttonVariants}
animate={isOpen ? 'open' : 'closed'}
transition={{ duration: 0.3 }}
>
<div className={styles.hamburger}>
<span className={`${styles.line} ${isOpen ? styles.line1Open : ''}`}></span>
<span className={`${styles.line} ${isOpen ? styles.line2Open : ''}`}></span>
<span className={`${styles.line} ${isOpen ? styles.line3Open : ''}`}></span>
</div>
</motion.button>
<AnimatePresence>
{isOpen && (
<motion.div
className={styles.menuOverlay}
variants={menuVariants}
initial="closed"
animate="open"
exit="closed"
>
<nav className={styles.mobileNav}>
<ul>
{['Home', 'About', 'Services', 'Contact'].map((item) => (
<motion.li key={item} variants={menuItemVariants}>
<Link href={item === 'Home' ? '/' : `/${item.toLowerCase()}`}>
{item}
</Link>
</motion.li>
))}
</ul>
</nav>
</motion.div>
)}
</AnimatePresence>
</div>
);
}/* components/MobileMenu.module.css */
.mobileMenuContainer {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.menuButton {
background-color: #3a86ff;
border: none;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 1001;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.hamburger {
width: 24px;
height: 18px;
position: relative;
}
.line {
position: absolute;
width: 100%;
height: 2px;
background-color: white;
transition: all 0.3s ease;
}
.line:nth-child(1) {
top: 0;
}
.line:nth-child(2) {
top: 8px;
}
.line:nth-child(3) {
top: 16px;
}
.line1Open {
top: 8px;
transform: rotate(45deg);
}
.line2Open {
opacity: 0;
}
.line3Open {
top: 8px;
transform: rotate(-45deg);
}
.menuOverlay {
position: fixed;
top: 0;
right: 0;
width: 300px;
height: 100vh;
background-color: white;
box-shadow: -5px 0 15px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.mobileNav {
padding: 80px 40px;
}
.mobileNav ul {
list-style: none;
padding: 0;
margin: 0;
}
.mobileNav li {
margin-bottom: 20px;
}
.mobileNav a {
font-size: 24px;
color: #333;
text-decoration: none;
transition: color 0.3s ease;
}
.mobileNav a:hover {
color: #3a86ff;
}2. Animated Cards with a Flip Effect
// components/FlipCard.tsx
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import styles from './FlipCard.module.css';
interface FlipCardProps {
frontContent: React.ReactNode;
backContent: React.ReactNode;
}
export function FlipCard({ frontContent, backContent }: FlipCardProps) {
const [isFlipped, setIsFlipped] = useState(false);
const frontVariants = {
flipped: { rotateY: 180, opacity: 0 },
unflipped: { rotateY: 0, opacity: 1 }
};
const backVariants = {
flipped: { rotateY: 0, opacity: 1 },
unflipped: { rotateY: -180, opacity: 0 }
};
return (
<div
className={styles.flipCardContainer}
onClick={() => setIsFlipped(!isFlipped)}
>
<motion.div
className={styles.flipCardFront}
variants={frontVariants}
animate={isFlipped ? 'flipped' : 'unflipped'}
transition={{ duration: 0.6, type: 'spring', stiffness: 300, damping: 20 }}
>
{frontContent}
</motion.div>
<motion.div
className={styles.flipCardBack}
variants={backVariants}
animate={isFlipped ? 'flipped' : 'unflipped'}
transition={{ duration: 0.6, type: 'spring', stiffness: 300, damping: 20 }}
>
{backContent}
</motion.div>
</div>
);
}/* components/FlipCard.module.css */
.flipCardContainer {
width: 300px;
height: 400px;
perspective: 1000px;
cursor: pointer;
position: relative;
}
.flipCardFront,
.flipCardBack {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
}
.flipCardFront {
background-color: #f9f9f9;
color: #333;
}
.flipCardBack {
background-color: #3a86ff;
color: white;
}3. Scroll Progress Bar
// components/ScrollProgressBar.tsx
'use client';
import { useState, useEffect } from 'react';
import { motion, useScroll } from 'framer-motion';
import styles from './ScrollProgressBar.module.css';
export function ScrollProgressBar() {
const { scrollYProgress } = useScroll();
return (
<motion.div
className={styles.progressBar}
style={{ scaleX: scrollYProgress }}
/>
);
}/* components/ScrollProgressBar.module.css */
.progressBar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 5px;
background-color: #3a86ff;
transform-origin: 0%;
z-index: 1000;
}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
Avoid animating:
width,heighttop,left,right,bottommargin,padding
2. Use will-change Carefully
.animated-element {
will-change: transform, opacity;
/* Rest of styles */
}Note: Overusing
will-changecan worsen performance.
3. Lazy Loading Heavy Animation Libraries
// components/AnimatedSection.tsx
'use client';
import { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
// Lazy loading Framer Motion
const motion = dynamic(() => import('framer-motion').then((mod) => mod.motion), {
ssr: false,
loading: () => <div>Loading...</div>,
});
export function AnimatedSection({ children }) {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) {
return <div>{children}</div>;
}
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
);
}4. Using GPU for Animations
.accelerated {
transform: translateZ(0); /* Enables GPU acceleration */
}Animation Accessibility
Remember that some users may prefer reduced animations, e.g., due to vestibular disorders.
1. Respecting System Preferences
@media (prefers-reduced-motion: reduce) {
/* Disable or simplify animations */
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}2. Implementing an Animation Toggle
// components/AnimationToggle.tsx
'use client';
import { useState, useEffect } from 'react';
import styles from './AnimationToggle.module.css';
export function AnimationToggle() {
const [areAnimationsEnabled, setAreAnimationsEnabled] = useState(true);
useEffect(() => {
// Check system preferences
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
setAreAnimationsEnabled(!prefersReducedMotion);
// Check saved user preferences
const savedPreference = localStorage.getItem('animations-enabled');
if (savedPreference !== null) {
setAreAnimationsEnabled(savedPreference === 'true');
}
}, []);
const toggleAnimations = () => {
const newValue = !areAnimationsEnabled;
setAreAnimationsEnabled(newValue);
document.documentElement.classList.toggle('reduce-animations', !newValue);
localStorage.setItem('animations-enabled', String(newValue));
};
return (
<button
className={styles.toggleButton}
onClick={toggleAnimations}
aria-pressed={!areAnimationsEnabled}
>
{areAnimationsEnabled ? 'Disable animations' : 'Enable animations'}
</button>
);
}/* app/globals.css */
.reduce-animations * {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}Scroll-dependent Animations
1. Basic Scroll Animation with Intersection Observer
// components/FadeInSection.tsx
'use client';
import { useRef, useEffect, useState } from 'react';
import styles from './FadeInSection.module.css';
export function FadeInSection({ children }) {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIsVisible(entry.isIntersecting);
},
{
root: null,
rootMargin: '0px',
threshold: 0.1,
}
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
if (ref.current) {
observer.unobserve(ref.current);
}
};
}, []);
return (
<div
ref={ref}
className={`${styles.fadeInSection} ${isVisible ? styles.isVisible : ''}`}
>
{children}
</div>
);
}/* components/FadeInSection.module.css */
.fadeInSection {
opacity: 0;
transform: translateY(30px);
transition: opacity 1s ease, transform 1s ease;
}
.isVisible {
opacity: 1;
transform: translateY(0);
}2. Advanced Scroll Animations with Framer Motion
// components/ScrollAnimation.tsx
'use client';
import { useRef } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';
import styles from './ScrollAnimation.module.css';
export function ScrollAnimation() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
});
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 0.8]);
const rotate = useTransform(scrollYProgress, [0, 0.5, 1], [-15, 0, 15]);
return (
<div ref={ref} className={styles.container}>
<motion.div
className={styles.animatedBox}
style={{ opacity, scale, rotate }}
>
<h2>Scroll animation</h2>
<p>This section animates based on scroll position.</p>
</motion.div>
</div>
);
}/* components/ScrollAnimation.module.css */
.container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.animatedBox {
padding: 40px;
background-color: #3a86ff;
color: white;
border-radius: 16px;
max-width: 500px;
text-align: center;
}Use Cases in Different Contexts
1. Animated Data Tables
// components/DataTable.tsx
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import styles from './DataTable.module.css';
interface DataItem {
id: string;
name: string;
value: number;
category: string;
}
interface DataTableProps {
data: DataItem[];
}
export function DataTable({ data }: DataTableProps) {
const [sortField, setSortField] = useState<keyof DataItem>('name');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const handleSort = (field: keyof DataItem) => {
if (field === sortField) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
};
const sortedData = [...data].sort((a, b) => {
if (a[sortField] < b[sortField]) return sortDirection === 'asc' ? -1 : 1;
if (a[sortField] > b[sortField]) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
return (
<div className={styles.tableContainer}>
<table className={styles.table}>
<thead>
<tr>
<th onClick={() => handleSort('name')}>
Name {sortField === 'name' && (sortDirection === 'asc' ? '↑' : '↓')}
</th>
<th onClick={() => handleSort('value')}>
Value {sortField === 'value' && (sortDirection === 'asc' ? '↑' : '↓')}
</th>
<th onClick={() => handleSort('category')}>
Category {sortField === 'category' && (sortDirection === 'asc' ? '↑' : '↓')}
</th>
</tr>
</thead>
<motion.tbody layout>
<AnimatePresence>
{sortedData.map((item) => (
<motion.tr
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
layout
>
<td>{item.name}</td>
<td>{item.value}</td>
<td>{item.category}</td>
</motion.tr>
))}
</AnimatePresence>
</motion.tbody>
</table>
</div>
);
}2. Animated Chat Interface
// components/ChatInterface.tsx
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import styles from './ChatInterface.module.css';
interface Message {
id: number;
text: string;
sender: 'user' | 'assistant';
timestamp: Date;
}
export function ChatInterface() {
const [messages, setMessages] = useState<Message[]>([]);
const [inputText, setInputText] = useState('');
const handleSend = () => {
if (!inputText.trim()) return;
// Add user message
const userMessage: Message = {
id: Date.now(),
text: inputText,
sender: 'user',
timestamp: new Date(),
};
setMessages([...messages, userMessage]);
setInputText('');
// Simulate assistant response
setTimeout(() => {
const assistantMessage: Message = {
id: Date.now() + 1,
text: `I received your message: "${inputText}"`,
sender: 'assistant',
timestamp: new Date(),
};
setMessages((prev) => [...prev, assistantMessage]);
}, 1000);
};
return (
<div className={styles.chatContainer}>
<div className={styles.chatMessages}>
<AnimatePresence>
{messages.map((message) => (
<motion.div
key={message.id}
className={`${styles.message} ${styles[message.sender]}`}
initial={{ opacity: 0, y: 20, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.3 }}
>
<div className={styles.messageContent}>
{message.text}
</div>
<div className={styles.timestamp}>
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</motion.div>
))}
</AnimatePresence>
</div>
<div className={styles.chatInput}>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
placeholder="Write a message..."
/>
<button onClick={handleSend}>Send</button>
</div>
</div>
);
}3. Animated Theme Selection
// components/ThemeToggle.tsx
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import styles from './ThemeToggle.module.css';
export function ThemeToggle() {
const [isDarkMode, setIsDarkMode] = useState(false);
useEffect(() => {
// Check system preferences or previously saved settings
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const savedTheme = localStorage.getItem('theme');
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
setIsDarkMode(initialTheme === 'dark');
document.documentElement.classList.toggle('dark-theme', initialTheme === 'dark');
}, []);
const toggleTheme = () => {
const newMode = !isDarkMode;
setIsDarkMode(newMode);
document.documentElement.classList.toggle('dark-theme', newMode);
localStorage.setItem('theme', newMode ? 'dark' : 'light');
};
return (
<button
className={styles.themeToggle}
onClick={toggleTheme}
aria-label={isDarkMode ? 'Switch to light theme' : 'Switch to dark theme'}
>
<motion.div
className={styles.toggleTrack}
animate={{ backgroundColor: isDarkMode ? '#3a86ff' : '#ccc' }}
>
<motion.div
className={styles.toggleThumb}
animate={{
x: isDarkMode ? 22 : 0,
backgroundColor: isDarkMode ? '#121212' : '#fff',
}}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
>
<motion.div
className={styles.iconContainer}
animate={{ opacity: isDarkMode ? 1 : 0 }}
>
{/* Moon icon */}
◐
</motion.div>
<motion.div
className={styles.iconContainer}
animate={{ opacity: isDarkMode ? 0 : 1 }}
>
{/* Sun icon */}
●
</motion.div>
</motion.div>
</motion.div>
</button>
);
}Best Practices for Creating Animations
- 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
// Dark Mode & Theme System - Quantum Metropolis
'use client';
import { useState } from 'react';
export default function DarkModeDemo() {
const [theme, setTheme] = useState<'dark' | 'light' | 'neon'>('dark');
const themes = {
dark: {
background: 'linear-gradient(135deg, #0f0f23, #1a1a2e)',
surface: 'rgba(255,255,255,0.05)',
surfaceBorder: 'rgba(100,255,218,0.2)',
text: '#ffffff',
textSecondary: '#b0bec5',
primary: '#64ffda',
secondary: '#7c4dff',
cardBg: 'rgba(15,15,35,0.9)'
},
light: {
background: 'linear-gradient(135deg, #f8f9fa, #e9ecef)',
surface: 'rgba(255,255,255,0.9)',
surfaceBorder: 'rgba(0,0,0,0.1)',
text: '#212529',
textSecondary: '#6c757d',
primary: '#0056cc',
secondary: '#4400cc',
cardBg: '#ffffff'
},
neon: {
background: 'linear-gradient(135deg, #000000, #1a0033)',
surface: 'rgba(255,0,128,0.1)',
surfaceBorder: 'rgba(255,0,128,0.5)',
text: '#ff0080',
textSecondary: '#ff69b4',
primary: '#ff0080',
secondary: '#00ff7f',
cardBg: 'rgba(0,0,0,0.8)'
}
};
const currentTheme = themes[theme];
return (
<div style={{
minHeight: '100vh',
background: currentTheme.background,
color: currentTheme.text,
padding: '3rem 2rem',
transition: 'all 0.5s ease'
}}>
{/* Theme Toggle */}
<div style={{textAlign: 'center', marginBottom: '3rem'}}>
<h1 style={{
fontSize: '3rem',
background: `linear-gradient(45deg, ${currentTheme.primary}, ${currentTheme.secondary})`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: '2rem'
}}>
Dark Mode & Theming
</h1>
<div style={{display: 'flex', gap: '1rem', justifyContent: 'center', flexWrap: 'wrap'}}>
<button
onClick={() => setTheme('dark')}
style={{
padding: '0.75rem 2rem',
borderRadius: '0.75rem',
border: `2px solid ${theme === 'dark' ? currentTheme.primary : currentTheme.surfaceBorder}`,
background: theme === 'dark' ? currentTheme.primary : 'transparent',
color: theme === 'dark' ? '#0f0f23' : currentTheme.text,
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.3s ease'
}}
>
Dark
</button>
<button
onClick={() => setTheme('light')}
style={{
padding: '0.75rem 2rem',
borderRadius: '0.75rem',
border: `2px solid ${theme === 'light' ? currentTheme.primary : currentTheme.surfaceBorder}`,
background: theme === 'light' ? currentTheme.primary : 'transparent',
color: theme === 'light' ? '#ffffff' : currentTheme.text,
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.3s ease'
}}
>
Light
</button>
<button
onClick={() => setTheme('neon')}
style={{
padding: '0.75rem 2rem',
borderRadius: '0.75rem',
border: `2px solid ${theme === 'neon' ? currentTheme.primary : currentTheme.surfaceBorder}`,
background: theme === 'neon' ? currentTheme.primary : 'transparent',
color: theme === 'neon' ? '#000000' : currentTheme.text,
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.3s ease'
}}
>
Neon
</button>
</div>
</div>
{/* CSS Variables Example */}
<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'}}>
<h2 style={{color: currentTheme.primary, marginBottom: '1.5rem'}}>CSS Custom Properties</h2>
<pre style={{background: 'rgba(0,0,0,0.3)', padding: '1.5rem', borderRadius: '0.75rem', overflow: 'auto', color: currentTheme.textSecondary, fontSize: '0.875rem'}}>
{`:root {
--color-bg: ${currentTheme.background};
--color-surface: ${currentTheme.surface};
--color-text: ${currentTheme.text};
--color-primary: ${currentTheme.primary};
--color-secondary: ${currentTheme.secondary};
}
[data-theme="dark"] {
--color-bg: #0f0f23;
--color-text: #ffffff;
}
[data-theme="light"] {
--color-bg: #ffffff;
--color-text: #212529;
}`}
</pre>
</section>
{/* Component Examples */}
<section style={{maxWidth: '1200px', margin: '0 auto 3rem'}}>
<h2 style={{color: currentTheme.primary, marginBottom: '2rem', textAlign: 'center'}}>Themed Components</h2>
<div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '2rem'}}>
<div style={{background: currentTheme.cardBg, padding: '2rem', borderRadius: '1rem', border: `1px solid ${currentTheme.surfaceBorder}`, transition: 'all 0.5s ease'}}>
<div style={{fontSize: '3rem', marginBottom: '1rem', textAlign: 'center'}}>↯</div>
<h3 style={{color: currentTheme.primary, marginBottom: '0.75rem', textAlign: 'center', fontSize: '1.5rem'}}>Energy System</h3>
<p style={{color: currentTheme.textSecondary, lineHeight: 1.6, textAlign: 'center'}}>
Quantum energy management in the metropolis
</p>
<div style={{marginTop: '1.5rem', width: '100%', height: '8px', background: currentTheme.surface, borderRadius: '4px', overflow: 'hidden'}}>
<div style={{width: '78%', height: '100%', background: `linear-gradient(90deg, ${currentTheme.primary}, ${currentTheme.secondary})`, borderRadius: '4px'}}></div>
</div>
</div>
<div style={{background: currentTheme.cardBg, padding: '2rem', borderRadius: '1rem', border: `1px solid ${currentTheme.surfaceBorder}`, transition: 'all 0.5s ease'}}>
<div style={{fontSize: '3rem', marginBottom: '1rem', textAlign: 'center'}}>◆</div>
<h3 style={{color: currentTheme.primary, marginBottom: '0.75rem', textAlign: 'center', fontSize: '1.5rem'}}>AI Core</h3>
<p style={{color: currentTheme.textSecondary, lineHeight: 1.6, textAlign: 'center'}}>
Next-generation artificial intelligence
</p>
<div style={{marginTop: '1.5rem', width: '100%', height: '8px', background: currentTheme.surface, borderRadius: '4px', overflow: 'hidden'}}>
<div style={{width: '92%', height: '100%', background: `linear-gradient(90deg, ${currentTheme.primary}, ${currentTheme.secondary})`, borderRadius: '4px'}}></div>
</div>
</div>
<div style={{background: currentTheme.cardBg, padding: '2rem', borderRadius: '1rem', border: `1px solid ${currentTheme.surfaceBorder}`, transition: 'all 0.5s ease'}}>
<div style={{fontSize: '3rem', marginBottom: '1rem', textAlign: 'center'}}>◆</div>
<h3 style={{color: currentTheme.primary, marginBottom: '0.75rem', textAlign: 'center', fontSize: '1.5rem'}}>Network</h3>
<p style={{color: currentTheme.textSecondary, lineHeight: 1.6, textAlign: 'center'}}>
Global quantum network
</p>
<div style={{marginTop: '1.5rem', width: '100%', height: '8px', background: currentTheme.surface, borderRadius: '4px', overflow: 'hidden'}}>
<div style={{width: '65%', height: '100%', background: `linear-gradient(90deg, ${currentTheme.primary}, ${currentTheme.secondary})`, borderRadius: '4px'}}></div>
</div>
</div>
</div>
</section>
{/* System Preference Detection */}
<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'}}>
<h2 style={{color: currentTheme.primary, marginBottom: '1.5rem'}}>System Preference Detection</h2>
<pre style={{background: 'rgba(0,0,0,0.3)', padding: '1.5rem', borderRadius: '0.75rem', overflow: 'auto', color: currentTheme.textSecondary, fontSize: '0.875rem'}}>
{`@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f0f23;
--color-text: #ffffff;
}
}
@media (prefers-color-scheme: light) {
:root {
--color-bg: #ffffff;
--color-text: #212529;
}
}`}
</pre>
</section>
</div>
);
}Docs entries used in this lesson
Spotted a mistake in this lesson?
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):