Building a Modern Portfolio with Next.js 15
A comprehensive guide to creating a high-performance portfolio website using Next.js 15, TypeScript, and Tailwind CSS with advanced features like dark mode and scroll animations.
Creating a professional portfolio website is crucial for any developer looking to showcase their skills and attract potential clients or employers. In this comprehensive guide, I'll walk you through building a modern, high-performance portfolio using the latest web technologies.
Why Next.js 15?
Next.js 15 introduces several compelling features that make it an excellent choice for portfolio websites:
- App Router: The new App Router provides a more intuitive file-based routing system
- Server Components: Improved performance with server-side rendering by default
- TypeScript Integration: First-class TypeScript support out of the box
- Optimized Bundle: Smaller bundle sizes and faster loading times
- SEO Friendly: Built-in optimization for search engines
Project Architecture
Tech Stack Overview
const techStack = {
framework: "Next.js 15",
language: "TypeScript",
styling: "Tailwind CSS",
animations: "CSS + Intersection Observer",
deployment: "Vercel",
performance: "Lighthouse Score 95+"
};
Directory Structure
portfolio/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── globals.css
│ ├── components/
│ │ ├── Hero.tsx
│ │ ├── Navigation.tsx
│ │ ├── Projects.tsx
│ │ └── Contact.tsx
│ ├── hooks/
│ │ ├── useScrollReveal.ts
│ │ └── useCountUp.ts
│ └── contexts/
│ └── ThemeContext.tsx
├── public/
│ ├── projects/
│ └── resume.pdf
└── package.json
Key Features Implementation
1. Responsive Navigation
The navigation component adapts to different screen sizes and provides smooth scrolling:
'use client';
import { useState, useEffect } from 'react';
export default function Navigation() {
const [isScrolled, setIsScrolled] = useState(false);
const [activeSection, setActiveSection] = useState('hero');
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 50);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<nav className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
isScrolled ? 'bg-white/80 backdrop-blur-md shadow-lg' : 'bg-transparent'
}`}>
{/* Navigation content */}
</nav>
);
}
2. Dark Mode Implementation
Using React Context for theme management. For a detailed implementation guide, see my article on implementing dark mode with Tailwind CSS:
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: 'light' | 'dark';
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('system');
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
const root = window.document.documentElement;
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
setResolvedTheme(systemTheme);
root.classList.toggle('dark', systemTheme === 'dark');
} else {
setResolvedTheme(theme);
root.classList.toggle('dark', theme === 'dark');
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
{children}
</ThemeContext.Provider>
);
}
3. Scroll Reveal Animations
Custom hook for intersection observer animations:
import { useEffect, useRef } from 'react';
export function useScrollReveal(threshold = 0.1, delay = 0) {
const elementRef = useRef<HTMLElement>(null);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setTimeout(() => {
element.classList.add('animate-fadeInUp');
}, delay);
observer.unobserve(element);
}
},
{ threshold }
);
observer.observe(element);
return () => observer.disconnect();
}, [threshold, delay]);
return elementRef;
}
Performance Optimization
Image Optimization
Using Next.js Image component for optimal performance:
import Image from 'next/image';
export default function ProjectCard({ project }: { project: Project }) {
return (
<div className="project-card">
<Image
src={project.image}
alt={`${project.title} screenshot`}
width={600}
height={400}
className="w-full h-48 object-cover"
priority={project.featured}
/>
</div>
);
}
Bundle Analysis
Monitor bundle size and optimize imports:
npm install --save-dev @next/bundle-analyzer
# In next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// Next.js config
});
SEO and Meta Tags
Implement proper meta tags for better search engine optimization:
// app/layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Richard Joseph Porter | Full-Stack Developer',
description: 'Experienced full-stack developer specializing in modern web technologies...',
keywords: ['developer', 'portfolio', 'next.js', 'typescript'],
authors: [{ name: 'Richard Joseph Porter' }],
openGraph: {
title: 'Richard Joseph Porter | Full-Stack Developer',
description: 'Experienced full-stack developer specializing in modern web technologies...',
url: 'https://richardporter.dev',
siteName: 'Richard Joseph Porter',
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
},
],
locale: 'en_US',
type: 'website',
},
};
Deployment and Performance
Vercel Deployment
- Connect your GitHub repository to Vercel
- Configure build settings:
{
"buildCommand": "npm run build",
"outputDirectory": ".next",
"framework": "nextjs"
}
Performance Monitoring
Track Core Web Vitals:
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Analytics />
</body>
</html>
);
}
Results and Metrics
After implementing these optimizations, the portfolio achieves:
- Lighthouse Performance: 98/100
- First Contentful Paint: <1.5s
- Largest Contentful Paint: <2.5s
- Cumulative Layout Shift: <0.1
- Bundle Size: <200KB initial load
Conclusion
Building a modern portfolio with Next.js 15 provides an excellent foundation for showcasing your work while demonstrating your technical skills. The combination of TypeScript, Tailwind CSS, and thoughtful performance optimizations creates a professional, fast-loading website that stands out to potential clients and employers.
Once your portfolio is built, consider extending it with additional features like a blog system for sharing your insights and a secure contact form for client inquiries.
Key takeaways:
- Performance First: Optimize images, minimize bundle size, and leverage Next.js built-in optimizations
- User Experience: Implement smooth animations and responsive design
- SEO Optimization: Use proper meta tags and structured data
- Modern Stack: Stay current with the latest web technologies
- Accessibility: Ensure your portfolio is accessible to all users
The modern web development landscape offers incredible tools for creating impressive portfolio websites. By leveraging Next.js 15's capabilities and following best practices, you can build a portfolio that not only showcases your projects but also demonstrates your commitment to quality and performance.

Richard Joseph Porter
Full-stack developer with expertise in modern web technologies. Passionate about building scalable applications and sharing knowledge through technical writing.