Loading
Create a professional developer portfolio — from design to deployment — with animations, dark mode, and perfect Lighthouse scores.
In this tutorial, you'll build a complete developer portfolio site using Next.js and Tailwind CSS. It will feature smooth animations, responsive design, dark mode, and optimized performance — deployed to Vercel.
What you'll learn:
Clean up the default template:
Set up your color palette in globals.css:
A clean starting point with your design tokens defined.
Create a shared layout with a fixed navbar:
A fixed navbar with backdrop blur looks modern and stays accessible.
Design principles:
The hero is the first thing visitors see. Make it count.
Project cards with subtle animations feel polished and interactive.
Skills organized by category are scannable and professional.
Make it easy for people to reach you. Clear CTAs, no forms unless necessary.
No animation library needed — CSS can handle scroll-triggered reveals:
For scroll-triggered animations, use the IntersectionObserver API:
Usage:
Subtle animations make the site feel alive without being distracting.
Create a sitemap:
Good metadata helps your portfolio appear in search results and social shares.
Run a Lighthouse audit and target 100 on all scores:
Image optimization:
Font optimization:
Bundle analysis:
A slow portfolio is a bad first impression. Target 100 on Lighthouse.
Post-deploy checklist:
Your portfolio is live. Share the URL and start getting noticed.
What you built: A professional developer portfolio with responsive design, scroll animations, SEO optimization, and perfect performance — the kind of site that gets you interviews.
npx create-next-app@latest my-portfolio --typescript --tailwind --app --src-dir
cd my-portfolio# Remove default page content
# Edit src/app/page.tsx to start fresh
# Edit src/app/globals.css to add your theme@theme {
--color-bg: #0a0a0f;
--color-surface: rgba(255, 255, 255, 0.04);
--color-border: rgba(255, 255, 255, 0.08);
--color-text: #f0f0f0;
--color-text-dim: #a0a0a8;
--color-accent: #10b981;
--color-accent-hover: #34d399;
}// src/app/layout.tsx
import type { Metadata } from "next";
import { Navbar } from "@/components/Navbar";
export const metadata: Metadata = {
title: "Your Name — Developer",
description: "Full-stack developer building great software.",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark scroll-smooth">
<body className="bg-[var(--color-bg)] text-[var(--color-text)]">
<Navbar />
<main>{children}</main>
</body>
</html>
);
}// src/components/Navbar.tsx
import Link from "next/link";
export function Navbar() {
const links = [
{ href: "#projects", label: "Projects" },
{ href: "#about", label: "About" },
{ href: "#contact", label: "Contact" },
];
return (
<nav className="fixed top-0 z-50 w-full border-b border-[var(--color-border)] bg-[var(--color-bg)]/80 backdrop-blur-xl">
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
<Link href="/" className="text-lg font-semibold text-[var(--color-accent)]">
YN
</Link>
<div className="flex gap-6">
{links.map((link) => (
<a
key={link.href}
href={link.href}
className="text-sm text-[var(--color-text-dim)] transition-colors hover:text-[var(--color-text)]"
>
{link.label}
</a>
))}
</div>
</div>
</nav>
);
}// src/components/Hero.tsx
export function Hero() {
return (
<section className="flex min-h-screen items-center px-6 pt-20">
<div className="mx-auto max-w-5xl">
<p className="mb-4 text-sm font-medium tracking-wider text-[var(--color-accent)] uppercase">
Full-Stack Developer
</p>
<h1 className="mb-6 text-5xl leading-tight font-bold sm:text-7xl">
I build things
<br />
for the web.
</h1>
<p className="mb-8 max-w-xl text-lg leading-relaxed text-[var(--color-text-dim)]">
I'm a software engineer specializing in building accessible, performant web applications.
Currently focused on React, TypeScript, and Next.js.
</p>
<div className="flex gap-4">
<a
href="#projects"
className="rounded-lg bg-[var(--color-accent)] px-6 py-3 font-medium text-black transition-colors hover:bg-[var(--color-accent-hover)]"
>
View Projects
</a>
<a
href="#contact"
className="rounded-lg border border-[var(--color-border)] px-6 py-3 font-medium transition-colors hover:bg-[var(--color-surface)]"
>
Get in Touch
</a>
</div>
</div>
</section>
);
}// src/components/ProjectCard.tsx
import Image from "next/image";
interface ProjectCardProps {
title: string;
description: string;
tags: string[];
image: string;
liveUrl?: string;
githubUrl?: string;
}
export function ProjectCard({
title,
description,
tags,
image,
liveUrl,
githubUrl,
}: ProjectCardProps) {
return (
<div className="group overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] transition-all duration-300 hover:border-[var(--color-accent)]/30 hover:shadow-[var(--color-accent)]/5 hover:shadow-lg">
{/* Project screenshot */}
<div className="relative aspect-video overflow-hidden">
<Image
src={image}
alt={`${title} screenshot`}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
</div>
{/* Content */}
<div className="p-5">
<h3 className="mb-2 text-xl font-semibold">{title}</h3>
<p className="mb-4 text-sm leading-relaxed text-[var(--color-text-dim)]">{description}</p>
{/* Tags */}
<div className="mb-4 flex flex-wrap gap-2">
{tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-[var(--color-accent)]/10 px-3 py-1 text-xs text-[var(--color-accent)]"
>
{tag}
</span>
))}
</div>
{/* Links */}
<div className="flex gap-4">
{liveUrl && (
<a
href={liveUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-[var(--color-accent)] hover:underline"
>
Live Demo →
</a>
)}
{githubUrl && (
<a
href={githubUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-[var(--color-text-dim)] hover:text-[var(--color-text)]"
>
Source Code
</a>
)}
</div>
</div>
</div>
);
}// src/components/Projects.tsx
export function Projects() {
const projects = [
{
title: "Task Flow",
description:
"A project management tool with real-time collaboration, drag-and-drop boards, and automated workflows.",
tags: ["React", "TypeScript", "WebSocket", "PostgreSQL"],
image: "/projects/taskflow.png",
liveUrl: "https://taskflow.example.com",
githubUrl: "https://github.com/you/taskflow",
},
// Add more projects...
];
return (
<section id="projects" className="px-6 py-20">
<div className="mx-auto max-w-5xl">
<h2 className="mb-12 text-3xl font-bold">Selected Projects</h2>
<div className="grid gap-6 md:grid-cols-2">
{projects.map((project) => (
<ProjectCard key={project.title} {...project} />
))}
</div>
</div>
</section>
);
}// src/components/About.tsx
export function About() {
const skills = [
{ category: "Frontend", items: ["React", "Next.js", "TypeScript", "Tailwind CSS"] },
{ category: "Backend", items: ["Node.js", "PostgreSQL", "REST APIs", "GraphQL"] },
{ category: "Tools", items: ["Git", "Docker", "CI/CD", "Vercel"] },
];
return (
<section id="about" className="px-6 py-20">
<div className="mx-auto max-w-5xl">
<h2 className="mb-12 text-3xl font-bold">About Me</h2>
<div className="grid gap-12 md:grid-cols-2">
<div className="space-y-4 leading-relaxed text-[var(--color-text-dim)]">
<p>
I'm a software engineer with a passion for building tools that make people's lives
easier. I focus on writing clean, accessible, and performant code.
</p>
<p>
When I'm not coding, you can find me contributing to open source, writing technical
blog posts, or exploring new technologies.
</p>
</div>
<div className="space-y-6">
{skills.map((group) => (
<div key={group.category}>
<h3 className="mb-2 text-sm font-semibold tracking-wider text-[var(--color-accent)] uppercase">
{group.category}
</h3>
<div className="flex flex-wrap gap-2">
{group.items.map((skill) => (
<span
key={skill}
className="rounded-lg border border-[var(--color-border)] px-3 py-1.5 text-sm"
>
{skill}
</span>
))}
</div>
</div>
))}
</div>
</div>
</div>
</section>
);
}// src/components/Contact.tsx
export function Contact() {
return (
<section id="contact" className="px-6 py-20">
<div className="mx-auto max-w-5xl">
<h2 className="mb-4 text-3xl font-bold">Get in Touch</h2>
<p className="mb-8 max-w-lg text-[var(--color-text-dim)]">
I'm always open to discussing new projects, creative ideas, or opportunities to be part of
something great.
</p>
<div className="flex gap-4">
<a
href="mailto:you@example.com"
className="rounded-lg bg-[var(--color-accent)] px-6 py-3 font-medium text-black transition-colors hover:bg-[var(--color-accent-hover)]"
>
Say Hello
</a>
<a
href="https://github.com/yourusername"
target="_blank"
rel="noopener noreferrer"
className="rounded-lg border border-[var(--color-border)] px-6 py-3 transition-colors hover:bg-[var(--color-surface)]"
>
GitHub
</a>
<a
href="https://linkedin.com/in/yourusername"
target="_blank"
rel="noopener noreferrer"
className="rounded-lg border border-[var(--color-border)] px-6 py-3 transition-colors hover:bg-[var(--color-surface)]"
>
LinkedIn
</a>
</div>
</div>
</section>
);
}/* globals.css */
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fade-in-up 0.6s ease-out forwards;
}
/* Stagger children */
.stagger > *:nth-child(1) {
animation-delay: 0ms;
}
.stagger > *:nth-child(2) {
animation-delay: 100ms;
}
.stagger > *:nth-child(3) {
animation-delay: 200ms;
}
.stagger > *:nth-child(4) {
animation-delay: 300ms;
}"use client";
import { useEffect, useRef, useState } from "react";
export function useInView(threshold = 0.1) {
const ref = useRef<HTMLDivElement>(null);
const [isInView, setIsInView] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.unobserve(el);
}
},
{ threshold }
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold]);
return { ref, isInView };
}function Section({ children }) {
const { ref, isInView } = useInView();
return (
<div
ref={ref}
className={`transition-all duration-700 ${
isInView ? "translate-y-0 opacity-100" : "translate-y-8 opacity-0"
}`}
>
{children}
</div>
);
}// src/app/layout.tsx
export const metadata: Metadata = {
title: {
default: "Your Name — Developer Portfolio",
template: "%s | Your Name",
},
description: "Full-stack developer building accessible, performant web applications.",
keywords: ["developer", "portfolio", "react", "nextjs", "typescript"],
authors: [{ name: "Your Name" }],
openGraph: {
type: "website",
locale: "en_US",
url: "https://yoursite.com",
title: "Your Name — Developer Portfolio",
description: "Full-stack developer building great software.",
siteName: "Your Name",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Your Name — Developer Portfolio",
},
],
},
twitter: {
card: "summary_large_image",
title: "Your Name — Developer Portfolio",
description: "Full-stack developer building great software.",
images: ["/og-image.png"],
},
robots: {
index: true,
follow: true,
},
};// src/app/sitemap.ts
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://yoursite.com",
lastModified: new Date(),
priority: 1,
},
];
}// Always use next/image for automatic optimization
import Image from "next/image";
<Image
src="/projects/taskflow.png"
alt="TaskFlow screenshot"
width={800}
height={450}
placeholder="blur"
blurDataURL="data:image/png;base64,..."
priority={false} // true only for above-the-fold images
/>;// src/app/layout.tsx
import { DM_Sans, JetBrains_Mono } from "next/font/google";
const dmSans = DM_Sans({ subsets: ["latin"], variable: "--font-sans" });
const jetBrains = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono" });npm run build
# Check the output for large pages
# Target: < 100kB First Load JS for static pages# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Or connect your GitHub repo at vercel.com for auto-deploys