Back to blog

Build a Developer Portfolio with React Sections

Published on March 20, 2026·7 min read

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:

  1. Hero — who you are, what you do, one CTA
  2. Project Showcase — 3–6 best projects in a grid or bento layout
  3. About / Skills — background, tech stack, personality
  4. Timeline — work history and/or education, reverse chronological
  5. 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.

VA

Victor Aubague

Developer & creator of Incubator

Full-stack developer specialized in React, Next.js, and TypeScript. I built Incubator to help developers ship beautiful interfaces faster — all components are crafted from real client projects and production code.

LinkedIn
Build a Developer Portfolio with React Sections — Incubator