A developer portfolio is the most direct signal of what you can build. Recruiters spend an average of 6 seconds on a portfolio before deciding to dig deeper or move on. That means every section needs to earn its space — clear hierarchy, fast load times, and zero fluff. This guide walks through building a complete developer portfolio in React using pre-built sections, from the hero to the contact form.
The Section Stack
A high-converting developer portfolio follows a predictable structure. Each section has one job:
- Hero — who you are, what you do, one CTA
- Project Showcase — 3–6 best projects in a grid or bento layout
- About / Skills — background, tech stack, personality
- Timeline — work history and/or education, reverse chronological
- Contact — form or direct links, no friction
That's five sections. Not ten, not two. Five sections cover everything a hiring manager or potential client needs to see. Let's build each one.
1. Hero with Name and Title
The portfolio hero is simpler than a SaaS hero. No social proof bar, no secondary CTA, no badge. Just your name, your title, and one call-to-action:
interface PortfolioHeroProps {
name: string;
title: string;
description: string;
ctaLabel: string;
ctaUrl: string;
avatarUrl?: string;
}
export default function PortfolioHero({ name, title, description, ctaLabel, ctaUrl, avatarUrl }: PortfolioHeroProps) {
return (
<section
style={{
minHeight: "90vh",
display: "flex",
alignItems: "center",
padding: "var(--section-padding-y-lg) 0",
background: "var(--color-background)",
}}
>
<div style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
gap: "1.5rem",
}}>
{avatarUrl && (
<img
src={avatarUrl}
alt={name}
style={{ width: 96, height: 96, borderRadius: "50%", objectFit: "cover" }}
/>
)}
<h1 style={{
fontSize: "clamp(2.5rem, 5vw, 4rem)",
fontWeight: 700,
lineHeight: 1.1,
letterSpacing: "-0.03em",
color: "var(--color-foreground)",
}}>
{name}
</h1>
<p style={{
fontSize: "1.25rem",
color: "var(--color-accent)",
fontWeight: 500,
}}>
{title}
</p>
<p style={{
fontSize: "1.0625rem",
lineHeight: 1.7,
color: "var(--color-foreground-muted)",
maxWidth: "560px",
}}>
{description}
</p>
<a
href={ctaUrl}
style={{
display: "inline-flex",
alignItems: "center",
gap: "8px",
padding: "0.875rem 2rem",
borderRadius: "var(--radius-full)",
background: "var(--color-accent)",
color: "#fff",
fontWeight: 600,
fontSize: "0.9375rem",
textDecoration: "none",
}}
>
{ctaLabel}
</a>
</div>
</section>
);
}
The CTA should link to your projects section (#projects) or directly to your contact form (#contact) depending on your goal. If you're job hunting, "View my work" pointing to projects is the stronger choice.
2. Project Showcase Grid
The project grid is the most important section of your portfolio. Use a responsive CSS Grid with 2 columns on desktop and 1 on mobile:
interface Project {
title: string;
description: string;
tags: string[];
imageUrl: string;
liveUrl?: string;
repoUrl?: string;
}
export function ProjectGrid({ projects }: { projects: Project[] }) {
return (
<section id="projects" style={{ padding: "var(--section-padding-y) 0" }}>
<div style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
}}>
<h2 style={{ fontSize: "2rem", fontWeight: 700, marginBottom: "3rem" }}>
Selected Work
</h2>
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 400px), 1fr))",
gap: "2rem",
}}>
{projects.map((project) => (
<article
key={project.title}
style={{
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
overflow: "hidden",
background: "var(--color-surface)",
}}
>
<img
src={project.imageUrl}
alt={project.title}
style={{ width: "100%", height: 240, objectFit: "cover" }}
/>
<div style={{ padding: "1.5rem" }}>
<h3 style={{ fontSize: "1.25rem", fontWeight: 600 }}>{project.title}</h3>
<p style={{
fontSize: "0.9375rem",
color: "var(--color-foreground-muted)",
lineHeight: 1.6,
marginTop: "0.5rem",
}}>
{project.description}
</p>
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem", marginTop: "1rem" }}>
{project.tags.map((tag) => (
<span
key={tag}
style={{
padding: "0.25rem 0.75rem",
borderRadius: "var(--radius-full)",
background: "var(--color-accent-subtle)",
color: "var(--color-foreground-muted)",
fontSize: "0.8125rem",
}}
>
{tag}
</span>
))}
</div>
</div>
</article>
))}
</div>
</div>
</section>
);
}
Curate ruthlessly. Three excellent projects beat ten mediocre ones. Each project card should have: a screenshot (not a logo), a one-sentence description of what you built and why, and tech tags. Link to the live site when possible — recruiters want to click and see it working.
3. About and Skills Section
The about section humanizes you beyond a list of projects. Keep it concise — two short paragraphs maximum — and pair it with a skills grid:
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))",
gap: "1rem",
marginTop: "2rem",
}}>
{skills.map((skill) => (
<div
key={skill.name}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "0.5rem",
padding: "1.25rem",
borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
}}
>
{skill.icon}
<span style={{ fontSize: "0.8125rem", fontWeight: 500 }}>{skill.name}</span>
</div>
))}
</div>
Show 8–12 skills maximum. If you list 30 technologies, none of them look like strengths. Group them logically: languages, frameworks, tools. Use recognizable icons (from lucide-react or react-icons) instead of plain text — visual scanning is faster.
4. Timeline Section
A reverse-chronological timeline of your work experience gives recruiters the structured data they're looking for. The visual pattern is a vertical line with nodes:
interface TimelineItem {
date: string;
title: string;
company: string;
description: string;
}
export function Timeline({ items }: { items: TimelineItem[] }) {
return (
<div style={{ position: "relative", paddingLeft: "2rem" }}>
{/* Vertical line */}
<div
aria-hidden
style={{
position: "absolute",
left: "7px",
top: 0,
bottom: 0,
width: "2px",
background: "var(--color-border)",
}}
/>
{items.map((item) => (
<div key={`${item.company}-${item.date}`} style={{ position: "relative", paddingBottom: "2.5rem" }}>
{/* Node dot */}
<div
aria-hidden
style={{
position: "absolute",
left: "-2rem",
top: "4px",
width: "16px",
height: "16px",
borderRadius: "50%",
background: "var(--color-accent)",
border: "3px solid var(--color-background)",
}}
/>
<span style={{ fontSize: "0.8125rem", color: "var(--color-foreground-muted)" }}>
{item.date}
</span>
<h3 style={{ fontSize: "1.125rem", fontWeight: 600, marginTop: "0.25rem" }}>
{item.title}
</h3>
<p style={{ fontSize: "0.9375rem", color: "var(--color-accent)", fontWeight: 500 }}>
{item.company}
</p>
<p style={{
fontSize: "0.9375rem",
color: "var(--color-foreground-muted)",
lineHeight: 1.6,
marginTop: "0.5rem",
}}>
{item.description}
</p>
</div>
))}
</div>
);
}
Keep descriptions to 1–2 sentences each. Focus on impact ("Reduced load time by 40%") over responsibilities ("Responsible for frontend development").
5. Contact Form
The last section removes friction. A simple form with name, email, and message fields is enough. Don't ask for phone number, company size, or budget — this isn't a SaaS lead gen form:
<form
action="/api/contact"
method="POST"
style={{ display: "flex", flexDirection: "column", gap: "1rem", maxWidth: "480px" }}
>
<input
name="name"
placeholder="Name"
required
style={{
padding: "0.75rem 1rem",
borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
color: "var(--color-foreground)",
fontSize: "0.9375rem",
}}
/>
<input name="email" type="email" placeholder="Email" required style={{ /* same styles */ }} />
<textarea
name="message"
placeholder="Message"
rows={5}
required
style={{ /* same styles, resize: "vertical" */ }}
/>
<button
type="submit"
style={{
padding: "0.875rem 2rem",
borderRadius: "var(--radius-full)",
background: "var(--color-accent)",
color: "#fff",
fontWeight: 600,
border: "none",
cursor: "pointer",
}}
>
Send Message
</button>
</form>
Add social links (GitHub, LinkedIn, Twitter/X) next to or below the form. Some visitors prefer to reach out via DM rather than fill out a form.
Assembling the Full Portfolio
Each section above is independent and self-contained. Drop them into a single page component in order: Hero, Projects, About, Timeline, Contact. Add a sticky navbar with anchor links (#projects, #about, #experience, #contact) and a footer, and you have a complete portfolio.
Instead of building every section from scratch, browse the Incubator hero catalog for polished hero variants, the portfolio catalog for project showcase layouts, the about section catalog for bio and skills sections, and the contact catalog for form designs. If you want a complete portfolio assembled from pre-built sections, the portfolio page builder lets you pick sections and export a full page. Every section is TypeScript, Tailwind CSS v4, and Next.js 15 ready — paste, customize your content, and deploy.