Animated Portfolio with Magic UI

Build an animated portfolio landing page with Next.js and Magic UI.

Introduction

30 Second Summary

Your portfolio is the first thing a recruiter or hiring manager sees. A static page with plain text gets the job done, but an animated, polished landing page shows you can build modern frontends.

In this project, you will build an animated portfolio landing page using Next.js and Magic UI. You will scaffold a project from scratch, add animated components like gradient text and shimmer buttons, and compose a visually impressive page with interactive project cards.

What You'll Build

A single-page animated portfolio site with a hero section featuring gradient-animated text and a typing animation subtitle, interactive project cards that glow on hover, a shimmer call-to-action button, and a dot pattern background.

By the end of this project, you'll have:

  • A Next.js 16 project scaffolded with TypeScript and Tailwind CSS.
  • A hero section with an animated gradient heading, a typing animation subtitle, and a shimmer button call to action.
  • A responsive projects section using MagicCard components with interactive hover effects and a DotPattern animated background.
  • Secret Mission: Add a dark/light theme toggle so visitors can switch the color scheme on the fly.

Are there any prerequisites?

Basic familiarity with the Next.js App Router (file-based routing, layouts, pages) is helpful. No prior Magic UI or Tailwind CSS experience is needed.

Scaffold Your Next.js Project and Initialize Magic UI

Every great portfolio starts with a solid foundation. Before you add animations or visual effects, you need a project structure that supports modern frontend development.

In this step, you'll scaffold a Next.js project with TypeScript and Tailwind CSS already configured. Then you'll initialize Magic UI through the shadcn CLI, which copies animated component source code directly into your project.

In this step, get ready to:
  • Scaffold a Next.js project with TypeScript and Tailwind CSS.
  • Initialize the shadcn CLI and install five Magic UI components.
  • Start the dev server and confirm the app loads in your browser.
Create the Next.js app with pnpm

The create-next-app scaffold generates a full project structure with routing, fonts, and styling pre-wired. You'll customize the options to match what this portfolio project needs.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Scaffold a new Next.js project by running this command:
pnpm create next-app@latest my-portfolio

This launches an interactive wizard that asks you how to configure the project. Answer each prompt as follows:

  • Enter No for the recommended Next.js defaults prompt (you'll customize settings instead).
  • Enter Yes for the TypeScript prompt.
  • Select ESLint for the linter prompt.
  • Enter No for the React Compiler prompt.
  • Enter Yes for the Tailwind CSS prompt.
  • Enter No for the src/ directory prompt.
  • Enter Yes for the App Router prompt.
  • Enter No for the customize import alias prompt.

Once you answer all the prompts, pnpm automatically installs all project dependencies. You should see a success message when it finishes.

  • Confirm the project folder was created by running:
ls

You should see my-portfolio listed in the output.

Seeing an error during scaffolding?

If you see a "command not found" error for pnpm, make sure pnpm is installed globally. Run pnpm --version to check.

If the install hangs or fails with a network error, check your internet connection and try running the command again.

Why can't I scaffold my Next.js project?

  • Navigate into your new project folder by running:
cd my-portfolio
Initialize shadcn

Magic UI uses the same CLI as shadcn/ui to install components. Before you can add any components, you need to initialize the shadcn configuration in your project.

There is a known compatibility issue where pnpm create next-app sometimes generates a pnpm-workspace.yaml file that causes shadcn init to fail. You'll remove it first as a precaution.

  • Remove the pnpm-workspace.yaml file (if it exists) by running:
rm -f pnpm-workspace.yaml

What does the -f flag do?

The -f flag means "force." It tells rm to skip the error if the file doesn't exist. The command succeeds either way.

  • Initialize shadcn in your project by running:
pnpm dlx shadcn@latest init

The CLI detects your Next.js project and asks for a base color theme.

  • Select Neutral as the base color.

Once it finishes, you should see a success message in your terminal.

What did shadcn init create?

  • components.json stores the CLI configuration so future component installs know where to place files.
  • lib/utils.ts provides a cn() helper function for merging Tailwind CSS class names.
  • app/globals.css was updated with CSS theme variables for colors, borders, and other design tokens.

Seeing ERR_PNPM_ADDING_TO_ROOT?

This means the pnpm-workspace.yaml file still exists. Run rm pnpm-workspace.yaml and then try pnpm dlx shadcn@latest init again.

Help me fix the shadcn init error

Add the Magic UI components

The shadcn CLI can also install Magic UI components. Each command copies the component's source code into your components/ui/ folder and installs any required dependencies.

  • Add all five Magic UI components by running these commands one at a time:
pnpm dlx shadcn@latest add @magicui/animated-gradient-text
pnpm dlx shadcn@latest add @magicui/typing-animation
pnpm dlx shadcn@latest add @magicui/shimmer-button
pnpm dlx shadcn@latest add @magicui/magic-card
pnpm dlx shadcn@latest add @magicui/dot-pattern

What gets installed with these components?

Each command creates a .tsx file in components/ui/ with the full component source code. You own this code and can customize it freely.

Some components also pull in shared dependencies like motion (for animations) and next-themes (for dark mode detection). These are added to your package.json automatically.

  • Confirm all five component files were created by running:
ls components/ui/

You should see animated-gradient-text.tsx, typing-animation.tsx, shimmer-button.tsx, magic-card.tsx, and dot-pattern.tsx in the output.

Missing a component file?

If any file is missing, re-run the matching pnpm dlx shadcn@latest add @magicui/... command. Check for typos in the component name.

If you see network timeout errors, wait a moment and try again.

Help me troubleshoot my Magic UI install

  • Start the Next.js dev server by running:
pnpm dev
  • Open your browser and navigate to http://localhost:3000.

You should see the default Next.js starter page with the Vercel logo and a "Get started" message. Leave the dev server running. It will auto-refresh your browser as you make changes in the next steps.

Page not loading at localhost:3000?

Make sure the terminal still shows the dev server running. If you see an error about port 3000 being in use, stop the other process or use a different port with pnpm dev --port 3001.

If you see a blank page or a module error, try stopping the server with Ctrl+C and running pnpm dev again.

My Next.js dev server won't start

Your project is scaffolded and all five Magic UI components are ready to use. Next up, you'll build the hero section with an animated gradient heading, a typing animation, and a shimmer button.

Build the Hero Section

Your Next.js project is scaffolded and all five Magic UI components are installed. Now it's time to replace that default starter page with something that actually looks like a portfolio.

The hero section is the first thing visitors see when they land on your page. You'll compose it from three Magic UI components: an animated gradient heading, a typing animation subtitle, and a shimmering call-to-action button.

In this step, get ready to:
  • Replace the default starter page with an animated gradient heading.
  • Add a typing animation subtitle and a shimmer call-to-action button.
  • Configure the CSS keyframes that power the animations.
Clear the default page and add the animated heading

The default Next.js starter page is full of boilerplate links and images you don't need. You'll clear it out entirely and drop in your first Magic UI component.

  • Open app/page.tsx in VS Codium.
  • Select all the existing content and delete it.
  • Replace it with the following code:
import { AnimatedGradientText } from "@/components/ui/animated-gradient-text"

export default function Home() {
  return (
    // Full-screen container with dark background
    <main className="relative min-h-screen overflow-hidden bg-background text-foreground">
      {/* Hero Section */}
      <section className="relative z-10 flex flex-col items-center justify-center gap-6 px-4 pt-32 pb-20 text-center">
        <AnimatedGradientText className="text-4xl font-bold md:text-6xl">
          Hi, I&apos;m Your Name
        </AnimatedGradientText>
      </section>
    </main>
  )
}

What does this code do?

  • The <main> element is the full-screen container for the entire page. min-h-screen makes it fill the viewport, and bg-background uses the theme color set by shadcn.
  • The <section> uses flexbox utilities to center everything vertically and horizontally. z-10 ensures the hero content sits above background layers you'll add later.
  • AnimatedGradientText renders a heading with a color gradient that cycles through colors. The md:text-6xl class makes the text larger on medium-sized screens and above.
  • Save app/page.tsx.
  • Check your browser at http://localhost:3000. You should see a gradient-colored "Hi, I'm Your Name" heading centered on the page.

Why isn't the gradient animating yet?

The gradient colors are visible, but they won't cycle yet. Magic UI components need custom CSS keyframes to animate. You'll add those in substep 3.

Don't see the heading?

  • Make sure you deleted all the old starter content before pasting the new code. Leftover JSX can cause syntax errors.
  • Check your terminal for errors. A red error message usually means a typo in the import path or a missing closing tag.
  • Confirm your dev server is still running. If it stopped, run pnpm dev again from inside the my-portfolio folder.

Help me debug why my AnimatedGradientText heading isn't showing up in the browser.

Add the typing animation and shimmer button

A heading alone doesn't make a hero section. You'll add a subtitle that types out your roles one by one, and a glowing button that links to your GitHub profile.

  • In app/page.tsx, replace the entire file content with the following updated code:
import { AnimatedGradientText } from "@/components/ui/animated-gradient-text"
import { TypingAnimation } from "@/components/ui/typing-animation"
import { ShimmerButton } from "@/components/ui/shimmer-button"

export default function Home() {
  return (
    <main className="relative min-h-screen overflow-hidden bg-background text-foreground">
      {/* Hero Section */}
      <section className="relative z-10 flex flex-col items-center justify-center gap-6 px-4 pt-32 pb-20 text-center">
        {/* Animated gradient heading */}
        <AnimatedGradientText className="text-4xl font-bold md:text-6xl">
          Hi, I&apos;m Your Name
        </AnimatedGradientText>

        {/* Typing animation that cycles through role titles */}
        <TypingAnimation
          className="text-lg text-muted-foreground md:text-xl"
          words={["Frontend Developer", "React Enthusiast", "UI Designer"]}
          loop
        />

        {/* Shimmer CTA button linking to GitHub */}
        <a href="https://github.com" target="_blank" rel="noopener noreferrer">
          <ShimmerButton className="mt-4 shadow-2xl">
            <span className="text-sm font-medium text-white whitespace-pre-wrap lg:text-lg">
              View My GitHub
            </span>
          </ShimmerButton>
        </a>
      </section>
    </main>
  )
}

What's new in this code?

  • TypingAnimation takes a words array and types each string one letter at a time. The loop prop makes it cycle through the list endlessly.
  • ShimmerButton renders a button with a glowing shimmer effect. It's wrapped in an <a> tag so clicking it opens GitHub in a new tab.
  • target="_blank" opens the link in a new tab. rel="noopener noreferrer" is a security best practice that prevents the new page from accessing your page's window object.
  • Save app/page.tsx.
  • Check your browser. You should see the typing animation cycling through "Frontend Developer", "React Enthusiast", and "UI Designer" below the heading. The "View My GitHub" button appears below that.

Typing animation not showing?

  • Verify the import path matches exactly: @/components/ui/typing-animation. A wrong path causes a module-not-found error.
  • Make sure the words prop is an array of strings wrapped in curly braces: words={["...", "..."]}.

Help me figure out why my TypingAnimation or ShimmerButton isn't rendering.

✔️ Awesome, I've got everything!

Great. Double check you've saved app/page.tsx before moving on.

ⓧ I'd like to double check the full code

Here is the complete app/page.tsx so far. Compare it against yours line by line.

import { AnimatedGradientText } from "@/components/ui/animated-gradient-text"
import { TypingAnimation } from "@/components/ui/typing-animation"
import { ShimmerButton } from "@/components/ui/shimmer-button"

export default function Home() {
  return (
    <main className="relative min-h-screen overflow-hidden bg-background text-foreground">
      {/* Hero Section */}
      <section className="relative z-10 flex flex-col items-center justify-center gap-6 px-4 pt-32 pb-20 text-center">
        <AnimatedGradientText className="text-4xl font-bold md:text-6xl">
          Hi, I&apos;m Your Name
        </AnimatedGradientText>
        <TypingAnimation
          className="text-lg text-muted-foreground md:text-xl"
          words={["Frontend Developer", "React Enthusiast", "UI Designer"]}
          loop
        />
        <a href="https://github.com" target="_blank" rel="noopener noreferrer">
          <ShimmerButton className="mt-4 shadow-2xl">
            <span className="text-sm font-medium text-white whitespace-pre-wrap lg:text-lg">
              View My GitHub
            </span>
          </ShimmerButton>
        </a>
      </section>
    </main>
  )
}
Add the required CSS animations

Magic UI components rely on custom CSS keyframes for their animations. The shadcn CLI copies the component source code into your project, but it does not add the keyframes automatically. You need to add them yourself.

  • Open app/globals.css in VS Codium.
  • Scroll to the very end of the file, past all the existing Tailwind CSS imports and theme variables that shadcn generated.
  • Add the following block after all existing content:
@theme inline {
  /* Animation shorthand variables used by Magic UI components */
  --animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite
    alternate;
  --animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear;
  --animate-gradient: gradient 8s linear infinite;
  --animate-blink-cursor: blink-cursor 1.2s step-end infinite;

  /* ShimmerButton: slides the shimmer highlight across the button */
  @keyframes shimmer-slide {
    to {
      transform: translate(calc(100cqw - 100%), 0);
    }
  }
  /* ShimmerButton: spins the border glow around the button edge */
  @keyframes spin-around {
    0% {
      transform: translateZ(0) rotate(0);
    }
    15%,
    35% {
      transform: translateZ(0) rotate(90deg);
    }
    65%,
    85% {
      transform: translateZ(0) rotate(270deg);
    }
    100% {
      transform: translateZ(0) rotate(360deg);
    }
  }
  /* AnimatedGradientText: moves the gradient across the text */
  @keyframes gradient {
    to {
      background-position: var(--bg-size, 300%) 0;
    }
  }
  /* TypingAnimation: blinks the cursor at the end of the text */
  @keyframes blink-cursor {
    0%,
    100% {
      opacity: 1;
    }
    50% {
      opacity: 0;
    }
  }
}

What does this CSS block do?

The @theme inline directive is Tailwind CSS v4 syntax. It registers custom animation variables and keyframes so Tailwind can reference them in utility classes.

  • shimmer-slide and spin-around power the ShimmerButton's glowing highlight and rotating border effect.
  • gradient scrolls the color gradient across the AnimatedGradientText heading.
  • blink-cursor makes the typing cursor blink on and off in the TypingAnimation.
  • Save app/globals.css and check your browser.
  • You should see the gradient heading cycling through colors, the typing animation writing out role titles with a blinking cursor, and the "View My GitHub" button shimmering with a glowing highlight.

Animations still not working?

  • Make sure you pasted the @theme inline block at the very end of globals.css, after all the existing content. Pasting it inside an existing block can break the syntax.
  • Check your terminal for CSS parsing errors. A missing curly brace or typo in a keyframe name stops the entire block from loading.
  • Try a hard refresh in your browser. Press Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear the cached stylesheet.

Help me debug why my Magic UI animations aren't working after adding the CSS keyframes.

✔️ Awesome, I've got everything!

Great. Double check you've saved app/globals.css before moving on.

ⓧ I'd like to double check the full code

The block below should appear at the very end of your app/globals.css file, after all the Tailwind imports and shadcn theme variables. Do not replace the existing content.

@theme inline {
  --animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite
    alternate;
  --animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear;
  --animate-gradient: gradient 8s linear infinite;
  --animate-blink-cursor: blink-cursor 1.2s step-end infinite;

  @keyframes shimmer-slide {
    to {
      transform: translate(calc(100cqw - 100%), 0);
    }
  }
  @keyframes spin-around {
    0% {
      transform: translateZ(0) rotate(0);
    }
    15%,
    35% {
      transform: translateZ(0) rotate(90deg);
    }
    65%,
    85% {
      transform: translateZ(0) rotate(270deg);
    }
    100% {
      transform: translateZ(0) rotate(360deg);
    }
  }
  @keyframes gradient {
    to {
      background-position: var(--bg-size, 300%) 0;
    }
  }
  @keyframes blink-cursor {
    0%,
    100% {
      opacity: 1;
    }
    50% {
      opacity: 0;
    }
  }
}

Your hero section is looking sharp with animated text and a glowing button. Next up, you'll build a projects section with interactive cards that glow when you hover over them.

Build the Projects Section with Magic Cards

Your hero section is live with animated text and a shimmer button. Now it is time to show off your work.

In this step, you will add a grid of interactive project cards that glow when visitors hover over them. The MagicCard component from Magic UI produces this effect, but it needs a theme provider to work. You will set that up first, then build the card grid.

In this step, get ready to:
  • Set up a ThemeProvider in your layout so MagicCard can detect the current theme.
  • Add a responsive grid of project cards with an interactive gradient glow effect.
Set up the ThemeProvider in layout.tsx

The MagicCard component internally uses the next-themes library to detect whether the page is in dark or light mode. Without a ThemeProvider wrapping your app, MagicCard would throw a runtime error.

  • Open app/layout.tsx in VS Codium.
  • Replace the entire file content with the following code:
import type { Metadata } from "next"
import { Geist, Geist_Mono } from "next/font/google"
// Import ThemeProvider so MagicCard can detect dark/light mode
import { ThemeProvider } from "next-themes"
import "./globals.css"

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
})

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
})

// Update metadata to reflect your portfolio
export const metadata: Metadata = {
  title: "My Portfolio",
  description: "Built with Next.js and Magic UI",
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    // suppressHydrationWarning prevents a React warning from next-themes
    <html lang="en" suppressHydrationWarning>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {/* Wrap page content so components can read the current theme */}
        <ThemeProvider attribute="class" defaultTheme="dark">
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

What does this code do?

  • ThemeProvider from next-themes wraps all page content and provides theme information to any child component that asks for it.
  • attribute="class" tells next-themes to toggle dark mode using a CSS class on the <html> element. This is the pattern Tailwind CSS expects.
  • defaultTheme="dark" starts the site in dark mode, where the gradient glow effects look their best.
  • suppressHydrationWarning on the <html> tag prevents a React warning caused by next-themes modifying the element on the client side. This is expected behavior.
  • Save app/layout.tsx.
  • Check your browser at http://localhost:3000. The page background should turn dark (near-black) and the text should be white, because the default theme is now dark.

Page still looks the same?

  • Make sure you saved app/layout.tsx. Unsaved changes will not apply.
  • Check that you have defaultTheme="dark" on the ThemeProvider component. Without it, the theme defaults to the system setting which may not look different.
  • If you see a runtime error in the terminal, confirm the import { ThemeProvider } from "next-themes" line is at the top of the file.

Why isn't my dark theme showing after adding ThemeProvider?

✔️ Awesome, I've got everything!

Great. Double check you have saved app/layout.tsx before moving on.

ⓧ I'd like to double check the full code

Here is the complete app/layout.tsx file. Make sure your code matches this exactly.

import type { Metadata } from "next"
import { Geist, Geist_Mono } from "next/font/google"
import { ThemeProvider } from "next-themes"
import "./globals.css"

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
})

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
})

export const metadata: Metadata = {
  title: "My Portfolio",
  description: "Built with Next.js and Magic UI",
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ThemeProvider attribute="class" defaultTheme="dark">
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}
Add the projects data and MagicCard grid

With the ThemeProvider in place, the MagicCard component can now detect the theme and render its gradient glow effect correctly.

  • Open app/page.tsx in VS Codium.
  • Find the existing import lines at the top of the file.
  • Add this new import below them:
import { MagicCard } from "@/components/ui/magic-card"
  • Next, add this projects array between the import block and the export default function Home() line:
// Define your project data as an array of objects
const projects = [
  {
    title: "Weather Dashboard",
    description:
      "A real-time weather app built with React and the OpenWeather API. Features city search and 5-day forecasts.",
  },
  {
    title: "Task Tracker",
    description:
      "A drag-and-drop task management board built with Next.js. Supports multiple columns and local storage.",
  },
  {
    title: "Portfolio Site",
    description:
      "This very site! Built with Next.js and Magic UI to showcase animated components and modern styling.",
  },
]

What is this projects array?

This is a plain JavaScript array of objects, each with a title and description. Storing your project data separately from the JSX makes it easy to add, remove, or reorder cards later without touching the rendering code.

You will not see a change in the browser yet. The next code block uses this data to render project cards.

  • Inside the Home function, find the closing </section> tag of the hero section.
  • Add the following projects section below it, but still inside the <main> tag:
      {/* Projects Section */}
      <section className="relative z-10 mx-auto max-w-5xl px-4 py-20">
        <h2 className="mb-12 text-center text-3xl font-bold">My Projects</h2>
        {/* Responsive grid: 1 column on mobile, 3 on medium+ screens */}
        <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
          {projects.map((project) => (
            <MagicCard key={project.title} className="rounded-xl">
              <div className="p-6">
                <h3 className="mb-2 text-xl font-semibold">
                  {project.title}
                </h3>
                <p className="text-sm text-muted-foreground">
                  {project.description}
                </p>
              </div>
            </MagicCard>
          ))}
        </div>
      </section>

What does this code do?

  • projects.map() loops through the array and renders one MagicCard for each project object.
  • grid-cols-1 md:grid-cols-3 creates a responsive layout. Cards stack in a single column on small screens and spread across three columns on medium screens and larger.
  • Each MagicCard automatically adds an interactive gradient glow that follows your mouse cursor on hover.
  • Save app/page.tsx.
  • Check your browser. You should see a My Projects heading below the hero section, with three cards in a row.
  • Hover over each card. You should see a coloured gradient glow that follows your mouse cursor.

Cards not showing or no glow effect?

  • If you see a runtime error mentioning useTheme, make sure you completed the ThemeProvider setup in app/layout.tsx from the previous substep.
  • Check that the projects array is defined outside the Home function, between the imports and export default function Home().
  • Make sure the new <section> is inside <main> but after the hero </section> closing tag.

Help me debug my MagicCard project cards not rendering.

✔️ Awesome, I've got everything!

Great. Double check you have saved app/page.tsx before moving on.

ⓧ I'd like to double check the full code

Here is the complete app/page.tsx file at this point. Make sure your code matches this exactly.

import { AnimatedGradientText } from "@/components/ui/animated-gradient-text"
import { TypingAnimation } from "@/components/ui/typing-animation"
import { ShimmerButton } from "@/components/ui/shimmer-button"
import { MagicCard } from "@/components/ui/magic-card"

const projects = [
  {
    title: "Weather Dashboard",
    description:
      "A real-time weather app built with React and the OpenWeather API. Features city search and 5-day forecasts.",
  },
  {
    title: "Task Tracker",
    description:
      "A drag-and-drop task management board built with Next.js. Supports multiple columns and local storage.",
  },
  {
    title: "Portfolio Site",
    description:
      "This very site! Built with Next.js and Magic UI to showcase animated components and modern styling.",
  },
]

export default function Home() {
  return (
    <main className="relative min-h-screen overflow-hidden bg-background text-foreground">
      {/* Hero Section */}
      <section className="relative z-10 flex flex-col items-center justify-center gap-6 px-4 pt-32 pb-20 text-center">
        <AnimatedGradientText className="text-4xl font-bold md:text-6xl">
          Hi, I&apos;m Your Name
        </AnimatedGradientText>
        <TypingAnimation
          className="text-lg text-muted-foreground md:text-xl"
          words={["Frontend Developer", "React Enthusiast", "UI Designer"]}
          loop
        />
        <a href="https://github.com" target="_blank" rel="noopener noreferrer">
          <ShimmerButton className="mt-4 shadow-2xl">
            <span className="text-sm font-medium text-white whitespace-pre-wrap lg:text-lg">
              View My GitHub
            </span>
          </ShimmerButton>
        </a>
      </section>

      {/* Projects Section */}
      <section className="relative z-10 mx-auto max-w-5xl px-4 py-20">
        <h2 className="mb-12 text-center text-3xl font-bold">My Projects</h2>
        <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
          {projects.map((project) => (
            <MagicCard key={project.title} className="rounded-xl">
              <div className="p-6">
                <h3 className="mb-2 text-xl font-semibold">
                  {project.title}
                </h3>
                <p className="text-sm text-muted-foreground">
                  {project.description}
                </p>
              </div>
            </MagicCard>
          ))}
        </div>
      </section>
    </main>
  )
}

Your portfolio now has a hero section and a set of interactive project cards. Next up, you will add a dot pattern background and a footer to give the page a polished, finished feel.

Add the Dot Pattern Background and Final Polish

Your hero section grabs attention with animated text, and the project cards invite exploration with their glowing hover effect. The page is missing one thing: visual depth.

In this final step, you will layer a subtle dot pattern behind everything to give the page texture. You will also add a footer to round off the design.

In this step, get ready to:
  • Add a DotPattern animated background behind all sections.
  • Add a footer to the bottom of the page.
  • Review and test the complete portfolio page.
Add the DotPattern background

A solid background can feel flat, especially on a dark-themed page. The DotPattern component from Magic UI renders a repeating grid of tiny dots that sits behind your content and adds visual texture.

  • In app/page.tsx, find the import lines at the top of the file.
  • Add this new import below the existing MagicCard import:
import { DotPattern } from "@/components/ui/dot-pattern"
  • Now scroll down to the <main> tag inside the Home function.
  • Add the DotPattern component as the very first child inside <main>, right before the {/* Hero Section */} comment:
<DotPattern className="absolute inset-0 z-0 opacity-30 [mask-image:radial-gradient(ellipse_at_center,black_40%,transparent_80%)]" />

What does this code do?

  • absolute inset-0 z-0 stretches the dot pattern to fill the entire <main> container and places it on the lowest layer (z-0). Your hero and projects sections already use z-10, so they sit above the dots.
  • opacity-30 keeps the dots subtle so they add texture without competing with your content.
  • The mask-image CSS creates a radial fade-out. The dots are most visible at the center and gradually disappear toward the edges.
  • Save app/page.tsx.
  • Check your browser at http://localhost:3000. You should see a grid of small dots behind the hero and project sections, fading out toward the edges of the screen.

Don't see the dot pattern?

  • Make sure the DotPattern component is placed inside <main>, not outside it. It needs a positioned parent to fill.
  • Check that you added the import line at the top of the file. Without it, Next.js will show a compile error in the terminal.
  • The dots are subtle on a dark background. Look closely at the center of the page where the opacity is highest.

Still stuck? Ask for help.

Add a footer to the page

Every polished site needs a footer. You will add a simple one that credits the tools you used.

  • In app/page.tsx, scroll down to the closing </section> tag of the Projects Section.
  • Add this footer right below it, before the closing </main> tag:
      {/* Footer */}
      <footer className="relative z-10 py-8 text-center text-sm text-muted-foreground">
        Built with Next.js and Magic UI
      </footer>

What does this code do?

  • relative z-10 keeps the footer above the dot pattern background, just like the hero and projects sections.
  • text-muted-foreground is a Tailwind CSS utility from your shadcn theme. It gives the footer a softer color so it does not compete with the main content.
  • Save app/page.tsx.
  • Check your browser. You should see "Built with Next.js and Magic UI" centered at the bottom of the page in a soft, muted color.

Footer not visible?

  • Make sure the <footer> is inside <main>, not after it. If it is outside <main>, it will lose its styling context.
  • Scroll to the very bottom of the page. The footer sits below the project cards, so you may need to scroll down to see it.

Need a hand? Ask for help.

Review and test the complete page

All the pieces are in place. Time to walk through the full experience and make sure everything works together.

  • Save all open files and check your browser at http://localhost:3000.
  • Walk through this checklist to confirm everything is working:
  • The animated gradient heading displays and the gradient cycles through colors.
  • The typing animation cycles through "Frontend Developer", "React Enthusiast", and "UI Designer" with a blinking cursor.
  • The shimmer button has a glowing shimmer effect.
  • The three project cards show an interactive gradient glow when you hover over them.
  • The dot pattern is visible in the background with a radial fade toward the edges.
  • The footer text is visible at the bottom of the page.
  • The page is responsive. Resize the browser window to see the project cards stack on smaller screens.

Something not looking right?

If any animation is missing, the most common cause is a missing CSS keyframe in app/globals.css. Double check that the @theme inline block from Step 2 is still intact at the bottom of the file.

If the hover glow on the cards is not working, check your terminal for errors. The MagicCard component needs the ThemeProvider from Step 3 to be in place.

Use the full code tab below to compare your file line by line. Help me figure out what is different between my code and the expected code.

✔️ Awesome, I've got everything!

Great. Double check you have saved app/page.tsx.

ⓧ I'd like to double check the full code

Here is the complete app/page.tsx as it should look at the end of this step.

import { AnimatedGradientText } from "@/components/ui/animated-gradient-text"
import { TypingAnimation } from "@/components/ui/typing-animation"
import { ShimmerButton } from "@/components/ui/shimmer-button"
import { MagicCard } from "@/components/ui/magic-card"
import { DotPattern } from "@/components/ui/dot-pattern"

const projects = [
  {
    title: "Weather Dashboard",
    description:
      "A real-time weather app built with React and the OpenWeather API. Features city search and 5-day forecasts.",
  },
  {
    title: "Task Tracker",
    description:
      "A drag-and-drop task management board built with Next.js. Supports multiple columns and local storage.",
  },
  {
    title: "Portfolio Site",
    description:
      "This very site! Built with Next.js and Magic UI to showcase animated components and modern styling.",
  },
]

export default function Home() {
  return (
    <main className="relative min-h-screen overflow-hidden bg-background text-foreground">
      <DotPattern className="absolute inset-0 z-0 opacity-30 [mask-image:radial-gradient(ellipse_at_center,black_40%,transparent_80%)]" />

      {/* Hero Section */}
      <section className="relative z-10 flex flex-col items-center justify-center gap-6 px-4 pt-32 pb-20 text-center">
        <AnimatedGradientText className="text-4xl font-bold md:text-6xl">
          Hi, I&apos;m Your Name
        </AnimatedGradientText>
        <TypingAnimation
          className="text-lg text-muted-foreground md:text-xl"
          words={["Frontend Developer", "React Enthusiast", "UI Designer"]}
          loop
        />
        <a href="https://github.com" target="_blank" rel="noopener noreferrer">
          <ShimmerButton className="mt-4 shadow-2xl">
            <span className="text-sm font-medium text-white whitespace-pre-wrap lg:text-lg">
              View My GitHub
            </span>
          </ShimmerButton>
        </a>
      </section>

      {/* Projects Section */}
      <section className="relative z-10 mx-auto max-w-5xl px-4 py-20">
        <h2 className="mb-12 text-center text-3xl font-bold">My Projects</h2>
        <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
          {projects.map((project) => (
            <MagicCard key={project.title} className="rounded-xl">
              <div className="p-6">
                <h3 className="mb-2 text-xl font-semibold">
                  {project.title}
                </h3>
                <p className="text-sm text-muted-foreground">
                  {project.description}
                </p>
              </div>
            </MagicCard>
          ))}
        </div>
      </section>

      {/* Footer */}
      <footer className="relative z-10 py-8 text-center text-sm text-muted-foreground">
        Built with Next.js and Magic UI
      </footer>
    </main>
  )
}

Secret mission

Add a Dark/Light Theme Toggle

Your portfolio looks great in dark mode. But what if visitors prefer light mode? Since next-themes is already configured from Step 3, you are one component away from a fully working theme toggle.

Before you go

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The my-portfolio project folder on your Desktop (includes all source code, config files, and node_modules).
  • The Next.js dev server running on http://localhost:3000.
  • Magic UI component files copied into components/ui/ by the shadcn CLI.
  • Dependencies installed by pnpm (stored in node_modules inside the project folder).

Keep everything running

No action needed. Choose this if you are still actively building or want to keep experimenting with Magic UI components.

  • Your dev server stays running at http://localhost:3000. Any changes you save will auto-refresh in the browser.
  • Your project files remain on your Desktop in the my-portfolio folder.

Pause - I'll come back to this later

Stop the dev server to free up your terminal and the port, but keep all project files so you can pick up where you left off.

  • Go to the terminal where the dev server is running and press Ctrl+C to stop it.
  • Your project files stay on your Desktop. To resume later, open a terminal, navigate to the my-portfolio folder, and run pnpm dev.

Delete - I don't want to use this again

Remove all project files and free up disk space. You can always re-scaffold the project from scratch if you change your mind.

  • Go to the terminal where the dev server is running and press Ctrl+C to stop it.
  • Navigate to your Desktop and delete the entire my-portfolio folder.
  • That is all there is to it. No cloud resources, accounts, or external services need to be cleaned up.

Mission accomplished

That's a Wrap!

Nice work! You just built a polished, animated portfolio landing page from scratch using Next.js and Magic UI.

You've learned how to:

  • Scaffold a Next.js 16 project and initialize Magic UI via the shadcn CLI to add animated components to your project.
  • Build a hero section with an AnimatedGradientText heading, a TypingAnimation subtitle, and a ShimmerButton call-to-action.
  • Create a responsive projects section with interactive MagicCard hover effects and a DotPattern background layer with a radial fade mask.
  • Secret Mission: Add a dark/light theme toggle so visitors can switch the entire color scheme on the fly.

Ready to quiz yourself?