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

Icons, Favicon, and Visual Metadata in Next.js 16

15 min read Β·
In this lesson14

In Metropolis Quantum, the city's visual identity is carefully designed so that every element - from logos on government buildings, through district signage, to city applications - creates a consistent image. This visual identity helps residents and visitors recognize official city elements, builds trust, and creates a professional image.

In the world of web applications, icons, favicon, and visual metadata play a similar role. They are key to your application's recognizability, influence users' first impressions, and make it easier to identify your site among many open browser tabs. Next.js 16 offers extensive tools for managing these elements, which we will learn about in this chapter.

Importance of Icons and Visual Metadata

Favicon and Application Icons

Favicon (short for "favorite icon") is a small icon displayed in the browser tab, bookmarks list, and browsing history. It is the first visual element that users associate with your application, so it is worth ensuring its quality and recognizability.

In modern web applications, we need not only a basic favicon but an entire set of icons in different sizes that will be used in different contexts:

  • Browser tab icons
  • Application icons on mobile devices
  • Desktop application icons (PWA)
  • Icons in search results
  • Social media sharing icons

Visual Metadata

Visual metadata is information that determines how your page is presented when shared on social media, messaging apps, or search results. It includes:

  • Page title
  • Page description
  • Preview images/graphics
  • Theme colors
  • Application information (for PWAs)

Basic Favicon Configuration in Next.js 16

Next.js 16 offers a simplified way of configuring favicon and icons through the Metadata API, available since Next.js 13.2. Let's start with basic configuration.

Placing Icons in the public Directory

First, place icon files in the public directory:

public/
β”œβ”€β”€ favicon.ico           # Classic favicon (16x16 or 32x32)
β”œβ”€β”€ icon.png              # Main icon (can be used in metadata API)
β”œβ”€β”€ apple-icon.png        # Icon for Apple devices
└── icons/                # Directory with different icon sizes
    β”œβ”€β”€ icon-16x16.png
    β”œβ”€β”€ icon-32x32.png
    β”œβ”€β”€ icon-192x192.png
    └── icon-512x512.png

Basic Favicon Configuration

The simplest way is to place a favicon.ico file in the public directory. Next.js will automatically recognize this file and use it as the favicon:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="pl">
      <body>{children}</body>
    </html>
  );
}

In this case, you don't need to do anything else - the browser will automatically fetch /favicon.ico.

Configuration via the Metadata API

Next.js 16 offers advanced icon configuration through the Metadata API:

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Metropolis Quantum',
  description: 'Official portal of the city of the future',
  icons: {
    icon: '/icon.png',         // or an array: [{ url: '/icon-16x16.png', sizes: '16x16' }, ...]
    shortcut: '/shortcut-icon.png',
    apple: '/apple-icon.png',  // or an array: [{ url: '/apple-icon.png' }, ...]
    other: {
      rel: 'apple-touch-icon-precomposed',
      url: '/apple-touch-icon-precomposed.png',
    },
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="pl">
      <body>{children}</body>
    </html>
  );
}

Advanced Icon Configuration

For professional applications, it is worth preparing a complete set of icons in different sizes and formats.

Extended Icon Configuration

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  icons: {
    icon: [
      { url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
      { url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
      { url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
      { url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
    ],
    shortcut: '/shortcut-icon.png',
    apple: [
      { url: '/icons/apple-icon-57x57.png', sizes: '57x57', type: 'image/png' },
      { url: '/icons/apple-icon-72x72.png', sizes: '72x72', type: 'image/png' },
      { url: '/icons/apple-icon-114x114.png', sizes: '114x114', type: 'image/png' },
      { url: '/icons/apple-icon-180x180.png', sizes: '180x180', type: 'image/png' },
    ],
  },
};

Resulting HTML

The above configuration will generate the following tags in the <head> section of the HTML document:

<link rel="icon" href="/icons/icon-16x16.png" sizes="16x16" type="image/png" />
<link rel="icon" href="/icons/icon-32x32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="/icons/icon-192x192.png" sizes="192x192" type="image/png" />
<link rel="icon" href="/icons/icon-512x512.png" sizes="512x512" type="image/png" />
<link rel="shortcut icon" href="/shortcut-icon.png" />
<link rel="apple-touch-icon" href="/icons/apple-icon-57x57.png" sizes="57x57" type="image/png" />
<link rel="apple-touch-icon" href="/icons/apple-icon-72x72.png" sizes="72x72" type="image/png" />
<link rel="apple-touch-icon" href="/icons/apple-icon-114x114.png" sizes="114x114" type="image/png" />
<link rel="apple-touch-icon" href="/icons/apple-icon-180x180.png" sizes="180x180" type="image/png" />

Open Graph and Twitter Cards Metadata

Open Graph and Twitter Cards metadata determine how your page looks when shared on social platforms. Next.js 16 offers built-in support for this metadata.

Open Graph Configuration

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Metropolis Quantum',
  description: 'Official portal of the city of the future',
  openGraph: {
    title: 'Metropolis Quantum - City of the Future',
    description: "Discover tomorrow's technology in the city of Metropolis Quantum",
    url: 'https://metropolisquantum.pl',
    siteName: 'Metropolis Quantum',
    images: [
      {
        url: '/images/og-image.jpg', // 1200x630 px recommended size
        width: 1200,
        height: 630,
        alt: 'Metropolis Quantum - Panorama of the city of the future',
      },
    ],
    locale: 'pl_PL',
    type: 'website',
  },
};

Twitter Cards Configuration

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  twitter: {
    card: 'summary_large_image',
    title: 'Metropolis Quantum - City of the Future',
    description: "Discover tomorrow's technology in the city of Metropolis Quantum",
    siteId: '1467726470533754880', // Twitter account ID
    creator: '@MetropolisQ',
    creatorId: '1467726470533754880',
    images: ['/images/twitter-image.jpg'], // 800x418 px recommended size
  },
};

Testing Open Graph and Twitter Cards Metadata

After deployment, you can test your metadata using:

Dynamic Metadata

In Next.js 16, you can generate metadata dynamically based on path, URL parameters, or external data.

Dynamic Metadata for Subpages

// app/districts/[id]/page.tsx
import { Metadata, ResolvingMetadata } from 'next';

interface Props {
  params: Promise<{ id: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  // Fetch district data
  const districtId = (await params).id;
  const district = await getDistrictData(districtId);

  // Optionally you can merge with parent metadata
  const previousImages = (await parent).openGraph?.images || [];

  return {
    title: `${district.name} | Metropolis Quantum`,
    description: district.description,
    openGraph: {
      images: [
        {
          url: `/images/districts/${districtId}.jpg`,
          width: 1200,
          height: 630,
          alt: district.name,
        },
        ...previousImages,
      ],
    },
    twitter: {
      images: [`/images/districts/${districtId}-twitter.jpg`],
    },
  };
}

export default async function DistrictPage({ params }: Props) {
  // Page rendering
}

// Function fetching district data
async function getDistrictData(id: string) {
  // In a real application: fetch from an API or database
  return {
    name: 'Quantum District',
    description: 'The most technologically advanced district of Metropolis Quantum.',
  };
}

Web App Manifest

A web application manifest is a JSON file that provides information about your application for browsers and mobile devices. It is crucial for Progressive Web Apps (PWA).

Creating the manifest.json File

Place the manifest.json file in the public directory:

{
  "name": "Metropolis Quantum",
  "short_name": "MetroQ",
  "description": "Official portal of the city of the future",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#121212",
  "theme_color": "#3a86ff",
  "icons": [
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Manifest Configuration in Next.js

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  // Other metadata...

  // Manifest for PWA
  manifest: '/manifest.json',

  // Or define the manifest directly:
  /*
  manifest: {
    name: 'Metropolis Quantum',
    short_name: 'MetroQ',
    description: 'Official portal of the city of the future',
    start_url: '/',
    display: 'standalone',
    background_color: '#121212',
    theme_color: '#3a86ff',
    icons: [
      {
        src: '/icons/icon-192x192.png',
        sizes: '192x192',
        type: 'image/png',
        purpose: 'any maskable'
      },
      {
        src: '/icons/icon-512x512.png',
        sizes: '512x512',
        type: 'image/png'
      }
    ]
  }
  */
};

Other Visual and Functional Metadata

Theme Color

You set the theme color for mobile browsers in a separate viewport export. The themeColor field in the metadata object has been deprecated since Next.js 14:

// app/layout.tsx
import type { Viewport } from 'next';

export const viewport: Viewport = {
  // Theme color for mobile browsers
  themeColor: [
    { media: '(prefers-color-scheme: light)', color: '#3a86ff' },
    { media: '(prefers-color-scheme: dark)', color: '#1e429f' },
  ],

  // Or a single color:
  // themeColor: '#3a86ff',
};

Apple-specific Support

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  // Other metadata...

  // Settings for Safari on iOS
  appleWebApp: {
    title: 'Metropolis Quantum', // Alternative title for iOS
    statusBarStyle: 'black-translucent',
    startupImage: [
      {
        url: '/startup/apple-startup-2048x2732.png',
        media: '(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)',
      },
      {
        url: '/startup/apple-startup-1668x2388.png',
        media: '(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)',
      },
      // More sizes...
    ],
  },
};

Alternative Mobile Application

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  // Other metadata...

  // Links to mobile applications
  alternates: {
    'android-app': 'android-app://com.metropolisquantum/https/metropolisquantum.pl',
    'ios-app': 'ios-app://1234567890/https/metropolisquantum.pl',
  },
};

Generating Icons and Metadata

Manually creating all icon sizes can be tedious. Fortunately, there are tools that automate this process.

Icon Generation Tools

  1. Favicon Generator (https://realfavicongenerator.net/) - generates a complete set of icons from a single image
  2. PWA Asset Generator (https://github.com/onderceylan/pwa-asset-generator) - command-line tool for generating PWA icons

Example of Using PWA Asset Generator

# Installation
npm i -g pwa-asset-generator

# Generating icons
npx pwa-asset-generator ./logo.svg ./public/icons --manifest ./public/manifest.json --icon-only --favicon

Dynamic Open Graph Image Generation

Sometimes it is worth dynamically generating Open Graph images for different subpages. In Next.js 16, we can do this using Route Handlers or an opengraph-image.tsx file placed next to the page.

Route Handler Implementation for Dynamic OG Images

// app/api/og/route.tsx
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';

// No runtime = 'edge' export: in Next.js 16 ImageResponse runs in the default Node.js runtime,
// and the documentation marks the Edge Runtime as deprecated

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);

  // Get parameters from the URL
  const title = searchParams.get('title') || 'Metropolis Quantum';
  const description = searchParams.get('description') || 'City of the future';

  // Optionally: fetch additional data from a database or API
  // const data = await fetchData();

  // Generate the image
  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          fontSize: 40,
          color: 'white',
          background: 'linear-gradient(to bottom, #3a86ff, #1e429f)',
          width: '100%',
          height: '100%',
          padding: '50px 200px',
          textAlign: 'center',
          justifyContent: 'center',
          alignItems: 'center',
          flexDirection: 'column',
        }}
      >
        <img
          src={`${request.nextUrl.origin}/logo.png`}
          alt="Logo"
          width="200"
          height="200"
        />
        <h1 style={{ fontSize: 70 }}>{title}</h1>
        <p style={{ fontSize: 40 }}>{description}</p>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    }
  );
}

Using Dynamic OG Image in Metadata

// app/districts/[id]/page.tsx
import { Metadata } from 'next';

interface Props {
  params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const district = await getDistrictData((await params).id);

  const ogImageUrl = new URL(`/api/og`, 'https://metropolisquantum.pl');
  ogImageUrl.searchParams.append('title', district.name);
  ogImageUrl.searchParams.append('description', district.description);

  return {
    title: `${district.name} | Metropolis Quantum`,
    description: district.description,
    openGraph: {
      images: [ogImageUrl.toString()],
    },
    twitter: {
      images: [ogImageUrl.toString()],
    },
  };
}

Structured Data for SEO

Structured data helps search engines better understand your page's content and can influence how search results are displayed.

JSON-LD Implementation

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  // Other metadata...
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="pl">
      <head>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify({
              '@context': 'https://schema.org',
              '@type': 'Organization',
              name: 'Metropolis Quantum',
              url: 'https://metropolisquantum.pl',
              logo: 'https://metropolisquantum.pl/logo.png',
              contactPoint: {
                '@type': 'ContactPoint',
                telephone: '+48-123-456-789',
                contactType: 'customer service',
              },
              sameAs: [
                'https://facebook.com/metropolisquantum',
                'https://twitter.com/metropolisq',
                'https://instagram.com/metropolisquantum',
              ],
            }),
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Dynamic JSON-LD for Subpages

// components/JsonLd.tsx
interface JsonLdProps {
  data: any;
}

export function JsonLd({ data }: JsonLdProps) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}
// app/districts/[id]/page.tsx
import { JsonLd } from '@/components/JsonLd';

export default async function DistrictPage({ params }: { params: Promise<{ id: string }> }) {
  const district = await getDistrictData((await params).id);

  const jsonLdData = {
    '@context': 'https://schema.org',
    '@type': 'Place',
    name: district.name,
    description: district.description,
    address: {
      '@type': 'PostalAddress',
      addressLocality: 'Metropolis Quantum',
      addressRegion: 'MQ',
      postalCode: '00-000',
      addressCountry: 'PL',
    },
    geo: {
      '@type': 'GeoCoordinates',
      latitude: district.coordinates.lat,
      longitude: district.coordinates.lng,
    },
    image: `https://metropolisquantum.pl/images/districts/${(await params).id}.jpg`,
  };

  return (
    <div>
      <JsonLd data={jsonLdData} />
      <h1>{district.name}</h1>
      {/* Rest of content */}
    </div>
  );
}

Complete Implementation Examples

Metadata for E-commerce Applications

// app/layout.tsx
import type { Metadata, Viewport } from 'next';

export const metadata: Metadata = {
  metadataBase: new URL('https://metropolisquantum.pl'),
  title: {
    default: 'Metropolis Quantum Store',
    template: '%s | Metropolis Quantum Store',
  },
  description: 'Store with the latest technologies of the future',
  keywords: ['technology', 'future', 'metropolis', 'quantum', 'store'],
  authors: [{ name: 'Metropolis Quantum', url: 'https://metropolisquantum.pl' }],
  creator: 'Metropolis Quantum Team',
  publisher: 'Metropolis Quantum Corporation',

  // Icons
  icons: {
    icon: [
      { url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
      { url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
      { url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
      { url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
    ],
    apple: [
      { url: '/icons/apple-icon.png' },
    ],
  },

  // Open Graph
  openGraph: {
    type: 'website',
    siteName: 'Metropolis Quantum Store',
    title: 'Metropolis Quantum Store - Technology of the future today',
    description: 'Store with the latest technologies of the future from Metropolis Quantum',
    url: 'https://store.metropolisquantum.pl',
    images: [
      {
        url: '/images/og-default.jpg',
        width: 1200,
        height: 630,
        alt: 'Metropolis Quantum Store',
      },
    ],
    locale: 'pl_PL',
  },

  // Twitter
  twitter: {
    card: 'summary_large_image',
    title: 'Metropolis Quantum Store',
    description: 'Technology of the future today',
    images: ['/images/twitter-image.jpg'],
    creator: '@MetropolisQ',
  },

  // Robots
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },

  // Manifest PWA
  manifest: '/manifest.json',

  // Alternate versions
  alternates: {
    canonical: 'https://store.metropolisquantum.pl',
    languages: {
      'en-US': 'https://store.metropolisquantum.pl/en',
      'de-DE': 'https://store.metropolisquantum.pl/de',
    },
    media: {
      'only screen and (max-width: 640px)': 'https://m.store.metropolisquantum.pl',
    },
    types: {
      'application/rss+xml': 'https://store.metropolisquantum.pl/rss',
    },
  },

  // Settings for Apple applications
  appleWebApp: {
    title: 'MQ Store',
    statusBarStyle: 'black-translucent',
    capable: true,
  },

  // Other
  formatDetection: {
    telephone: true,
    date: false,
    address: true,
    email: true,
    url: true,
  },
};

// Theme color - in a separate viewport export, because themeColor in metadata is deprecated
export const viewport: Viewport = {
  themeColor: [
    { media: '(prefers-color-scheme: light)', color: '#3a86ff' },
    { media: '(prefers-color-scheme: dark)', color: '#1e429f' },
  ],
};

Metadata for the Product Page

// app/products/[id]/page.tsx
import { Metadata } from 'next';

interface Props {
  params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const product = await getProductData((await params).id);

  const price = new Intl.NumberFormat('pl-PL', {
    style: 'currency',
    currency: 'PLN',
  }).format(product.price);

  return {
    title: product.name,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      type: 'product',
      images: [
        {
          url: product.images[0].url,
          width: 800,
          height: 600,
          alt: product.name,
        },
      ],
      availability: product.inStock ? 'in stock' : 'out of stock',
      price: {
        amount: product.price.toString(),
        currency: 'PLN',
      },
    },
  };
}

export default async function ProductPage({ params }: Props) {
  const product = await getProductData((await params).id);

  const jsonLdData = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    description: product.description,
    image: product.images.map((img) => img.url),
    offers: {
      '@type': 'Offer',
      price: product.price,
      priceCurrency: 'PLN',
      availability: product.inStock
        ? 'https://schema.org/InStock'
        : 'https://schema.org/OutOfStock',
      url: `https://store.metropolisquantum.pl/products/${(await params).id}`,
    },
    brand: {
      '@type': 'Brand',
      name: 'Metropolis Quantum',
    },
    aggregateRating: {
      '@type': 'AggregateRating',
      ratingValue: product.rating.average,
      reviewCount: product.rating.count,
    },
  };

  return (
    <div>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdData) }}
      />
      <h1>{product.name}</h1>
      {/* Rest of product page content */}
    </div>
  );
}

async function getProductData(id: string) {
  // In a real application: fetch from an API or database
  return {
    id,
    name: 'Quantum communicator MQ-7',
    description: 'The latest communicator using quantum technology for instant communication over any distance.',
    price: 4999.99,
    inStock: true,
    images: [
      { url: '/images/products/mq-7-main.jpg' },
      { url: '/images/products/mq-7-side.jpg' },
      { url: '/images/products/mq-7-back.jpg' },
    ],
    rating: {
      average: 4.8,
      count: 156,
    },
  };
}

Best Practices

  1. Prepare a full set of icons - include all required sizes for different platforms and devices
  2. Optimize images - compress and optimize all icons and Open Graph images
  3. Test on different platforms - check how your metadata looks on Facebook, Twitter, WhatsApp, and other platforms
  4. Use meaningful file names - name your files descriptively (e.g., product-og-image.jpg instead of image1.jpg)
  5. Add structured data - use JSON-LD for better SEO
  6. Customize metadata for each subpage - generate dynamic metadata for each page
  7. Remember about alternative texts - always add the alt attribute to images
  8. Consider different modes (light/dark) - adjust theme colors to user preferences
  9. Provide icons in SVG format when possible - for better scalability

Debugging Metadata

Metadata Testing Tools

  1. Facebook Sharing Debugger - https://developers.facebook.com/tools/debug/
  2. Twitter Card Validator - https://cards-dev.twitter.com/validator
  3. LinkedIn Post Inspector - https://www.linkedin.com/post-inspector/
  4. Rich Results Test (Google) - https://search.google.com/test/rich-results

Debugging in Next.js

You can check the generated metadata directly in your page's HTML source. Check the <head> section in the browser's developer tools.

Summary

Icons, favicon, and visual metadata are a key element of your Next.js 16 application's visual identity. Just like in Metropolis Quantum, where every visual element builds a consistent image of the city, in your application carefully designed icons and metadata build a professional image and increase recognizability.

Key takeaways:

  1. Use the Next.js 16 Metadata API - simplifies managing all metadata
  2. Prepare a complete set of icons - for different platforms and devices
  3. Configure Open Graph and Twitter Cards - for better visibility on social media
  4. Add structured data - for better SEO and richer search results
  5. Generate dynamic metadata - customized for each subpage
  6. Test your metadata - check how they look on different platforms

In the next chapter, we will focus on animations and transitions in the user interface, which add dynamism and interactivity to Next.js 16 applications, just like holographic effects and smooth animations bring the architecture of Metropolis Quantum to life.

Code for this lesson: app/page.tsx
// CSS Animations & Transitions - Cyberpunk Metropolis
'use client';

export default function AnimationsDemo() {
  return (
    <div style={{
      minHeight: '100vh',
      background: 'linear-gradient(135deg, #0f0f23, #1a1a2e)',
      color: '#ffffff',
      padding: '3rem 2rem'
    }}>
      <h1 style={{
        fontSize: '3rem',
        textAlign: 'center',
        background: 'linear-gradient(45deg, #64ffda, #7c4dff)',
        WebkitBackgroundClip: 'text',
        WebkitTextFillColor: 'transparent',
        marginBottom: '3rem',
        animation: 'fadeIn 1s ease-out'
      }}>
        Animations & Transitions
      </h1>

      {/* Transition Examples */}
      <section style={{maxWidth: '1200px', margin: '0 auto 3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
        <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>CSS Transitions</h2>
        <div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '2rem'}}>
          <div style={{background: '#64ffda', padding: '2rem', borderRadius: '0.75rem', textAlign: 'center', color: '#0f0f23', fontWeight: 600, transition: 'all 0.3s ease', cursor: 'pointer'}} onMouseEnter={e => e.currentTarget.style.transform = 'translateY(-10px)'} onMouseLeave={e => e.currentTarget.style.transform = 'translateY(0)'}>
            Hover me!
          </div>
          <div style={{background: 'rgba(124,77,255,0.2)', padding: '2rem', borderRadius: '0.75rem', textAlign: 'center', border: '2px solid #7c4dff', color: '#7c4dff', fontWeight: 600, transition: 'all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55)', cursor: 'pointer'}} onMouseEnter={e => { e.currentTarget.style.transform = 'scale(1.1) rotate(5deg)'; e.currentTarget.style.background = 'rgba(124,77,255,0.4)'; }} onMouseLeave={e => { e.currentTarget.style.transform = 'scale(1) rotate(0deg)'; e.currentTarget.style.background = 'rgba(124,77,255,0.2)'; }}>
            Bounce Effect
          </div>
        </div>
      </section>

      {/* Keyframe Animations */}
      <section style={{maxWidth: '1200px', margin: '0 auto 3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
        <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>Keyframe Animations</h2>

        <div style={{display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '3rem', textAlign: 'center'}}>
          <div>
            <div style={{width: '100px', height: '100px', margin: '0 auto 1rem', background: 'linear-gradient(135deg, #64ffda, #448aff)', borderRadius: '50%', animation: 'pulse 2s ease-in-out infinite'}}></div>
            <p style={{color: '#b0bec5'}}>Pulse</p>
          </div>

          <div>
            <div style={{width: '100px', height: '100px', margin: '0 auto 1rem', background: 'linear-gradient(135deg, #7c4dff, #ff0080)', borderRadius: '0.75rem', animation: 'rotate 3s linear infinite'}}></div>
            <p style={{color: '#b0bec5'}}>Rotate</p>
          </div>

          <div>
            <div style={{width: '100px', height: '100px', margin: '0 auto 1rem', background: 'linear-gradient(135deg, #00ff7f, #ffdc00)', borderRadius: '50% 0', animation: 'bounce 1.5s ease-in-out infinite'}}></div>
            <p style={{color: '#b0bec5'}}>Bounce</p>
          </div>
        </div>
      </section>

      {/* Neon Glow Animation */}
      <section style={{maxWidth: '1200px', margin: '0 auto 3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
        <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>Neon Glow Effect</h2>
        <div style={{textAlign: 'center', padding: '3rem'}}>
          <h3 style={{fontSize: '4rem', fontWeight: 800, color: '#64ffda', textShadow: '0 0 10px #64ffda, 0 0 20px #64ffda, 0 0 30px #64ffda', animation: 'neonPulse 2s ease-in-out infinite'}}>
            QUANTUM
          </h3>
          <p style={{color: '#b0bec5', marginTop: '1rem', fontSize: '1.25rem', animation: 'fadeIn 2s ease-out'}}>
            Cyberpunk 2150
          </p>
        </div>
      </section>

      {/* Loading Animations */}
      <section style={{maxWidth: '1200px', margin: '0 auto 3rem', background: 'rgba(255,255,255,0.05)', padding: '2rem', borderRadius: '1rem', border: '1px solid rgba(100,255,218,0.2)'}}>
        <h2 style={{color: '#64ffda', marginBottom: '2rem'}}>Loading Animations</h2>
        <div style={{display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '3rem', textAlign: 'center'}}>
          <div>
            <div style={{width: '50px', height: '50px', margin: '0 auto 1rem', border: '4px solid rgba(100,255,218,0.3)', borderTop: '4px solid #64ffda', borderRadius: '50%', animation: 'spin 1s linear infinite'}}></div>
            <p style={{color: '#b0bec5'}}>Spinner</p>
          </div>

          <div>
            <div style={{display: 'flex', gap: '8px', justifyContent: 'center', marginBottom: '1rem'}}>
              <div style={{width: '12px', height: '12px', background: '#64ffda', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0s'}}></div>
              <div style={{width: '12px', height: '12px', background: '#7c4dff', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0.2s'}}></div>
              <div style={{width: '12px', height: '12px', background: '#ff0080', borderRadius: '50%', animation: 'dotPulse 1.4s ease-in-out infinite', animationDelay: '0.4s'}}></div>
            </div>
            <p style={{color: '#b0bec5'}}>Dots</p>
          </div>

          <div>
            <div style={{width: '100%', height: '4px', background: 'rgba(100,255,218,0.2)', borderRadius: '2px', overflow: 'hidden', margin: '0 auto 1rem'}}>
              <div style={{width: '50%', height: '100%', background: 'linear-gradient(90deg, #64ffda, #7c4dff)', borderRadius: '2px', animation: 'progress 2s ease-in-out infinite'}}></div>
            </div>
            <p style={{color: '#b0bec5'}}>Progress</p>
          </div>
        </div>
      </section>

      <style jsx global>{`
        @keyframes fadeIn {
          from { opacity: 0; transform: translateY(20px); }
          to { opacity: 1; transform: translateY(0); }
        }

        @keyframes pulse {
          0%, 100% { transform: scale(1); opacity: 1; }
          50% { transform: scale(1.1); opacity: 0.8; }
        }

        @keyframes rotate {
          from { transform: rotate(0deg); }
          to { transform: rotate(360deg); }
        }

        @keyframes bounce {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-30px); }
        }

        @keyframes neonPulse {
          0%, 100% { text-shadow: 0 0 10px #64ffda, 0 0 20px #64ffda, 0 0 30px #64ffda; }
          50% { text-shadow: 0 0 20px #64ffda, 0 0 40px #64ffda, 0 0 60px #64ffda; }
        }

        @keyframes spin {
          to { transform: rotate(360deg); }
        }

        @keyframes dotPulse {
          0%, 100% { transform: scale(0.8); opacity: 0.5; }
          50% { transform: scale(1.2); opacity: 1; }
        }

        @keyframes progress {
          0% { transform: translateX(-100%); }
          100% { transform: translateX(200%); }
        }
      `}</style>
    </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. 1. Dark mode in applications is best implemented using:

  2. 2. CSS Container Queries allow:

These are 2 of 13 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Vertical ordering

    Arrange the stages of configuring and loading fonts in Next.js (next/font) in the correct order:

  • Vertical ordering

    Arrange the steps for implementing dark mode in a Next.js application in the correct order:

  • Vertical ordering

    Arrange the elements in the correct order: 'use server' β†’ async function β†’ Component

  • Vertical ordering

    Arrange the elements in the correct order: 'use client' β†’ import { useState } β†’ function

  • Vertical ordering

    Arrange the manual Tailwind CSS 3 setup steps in a Next.js project

Useful articles