Build a Developer Portfolio Website with HTML, CSS, and JavaScript
Build a standout developer portfolio from scratch using HTML, CSS, and JavaScript -- responsive design, dark mode, project cards, and GitHub Pages deployment.
Your portfolio website is the most important project you'll ever build. Not because it's technically complex, but because it's the one project every potential employer, client, or collaborator will see. And yet, most developer portfolios are either over-engineered React apps for what should be a static page, or generic templates that look like everyone else's.
We're going to build a portfolio from scratch with just HTML, CSS, and JavaScript. No frameworks, no build tools, no npm install. The result will be fast, accessible, responsive, and actually memorable. And we'll deploy it for free on GitHub Pages.
Planning the Sections
Every good portfolio needs these sections:
- Hero -- who you are, what you do, one compelling sentence
- About -- brief background, what you're interested in, personality
- Projects -- your best work with descriptions and links
- Skills -- technologies you work with (keep it honest)
- Contact -- how to reach you
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Jane Developer - Full-stack developer specializing in React, Node.js, and Python. Building things that matter.">
<title>Jane Developer | Full-Stack Developer</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" href="favicon.ico" type="image/x-icon">
</head>
<body>
<header class="header">
<nav class="nav">
<a href="#" class="nav-logo">JD</a>
<ul class="nav-links">
<li><a href="#about">About</a></li>
<li><a href="#projects">Projects</a></li>
<li><a href="#skills">Skills</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark mode">
<span class="theme-toggle-icon"></span>
</button>
<button class="mobile-menu-btn" id="mobileMenuBtn" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
</nav>
</header>
<main>
<!-- Hero Section -->
<section class="hero" id="hero">
<div class="container">
<p class="hero-greeting">Hi, I'm</p>
<h1 class="hero-name">Jane Developer</h1>
<h2 class="hero-title">I build things for the web.</h2>
<p class="hero-description">
Full-stack developer focused on building accessible, performant
web applications. Currently interested in distributed systems
and developer tooling.
</p>
<div class="hero-cta">
<a href="#projects" class="btn btn-primary">View My Work</a>
<a href="#contact" class="btn btn-secondary">Get in Touch</a>
</div>
</div>
</section>
<!-- About Section -->
<section class="section" id="about">
<div class="container">
<h2 class="section-title">About Me</h2>
<div class="about-content">
<div class="about-text">
<p>
I'm a developer based in Portland who enjoys building things
that live on the internet. I got into programming through
tinkering with WordPress themes in college, and haven't stopped
building since.
</p>
<p>
I've worked at startups and agencies, building everything from
marketing sites to complex data dashboards. These days, I'm
particularly interested in how we can make developer tools
better and more accessible.
</p>
<p>
When I'm not coding, you'll find me hiking, reading science
fiction, or making unnecessarily complicated coffee.
</p>
</div>
<div class="about-image">
<img src="images/profile.jpg" alt="Jane Developer" loading="lazy">
</div>
</div>
</div>
</section>
<!-- Projects Section -->
<section class="section section-alt" id="projects">
<div class="container">
<h2 class="section-title">Things I've Built</h2>
<div class="projects-grid">
<article class="project-card">
<div class="project-image">
<img src="images/project-1.jpg" alt="TaskFlow app screenshot" loading="lazy">
</div>
<div class="project-content">
<h3 class="project-name">TaskFlow</h3>
<p class="project-description">
A project management tool built with React and Node.js.
Features real-time collaboration, kanban boards, and
integrations with GitHub and Slack.
</p>
<ul class="project-tech">
<li>React</li>
<li>Node.js</li>
<li>PostgreSQL</li>
<li>WebSocket</li>
</ul>
<div class="project-links">
<a href="https://github.com/jane/taskflow" target="_blank" rel="noopener noreferrer">GitHub</a>
<a href="https://taskflow-demo.com" target="_blank" rel="noopener noreferrer">Live Demo</a>
</div>
</div>
</article>
<article class="project-card">
<div class="project-image">
<img src="images/project-2.jpg" alt="DevMetrics dashboard screenshot" loading="lazy">
</div>
<div class="project-content">
<h3 class="project-name">DevMetrics</h3>
<p class="project-description">
A CLI tool that aggregates development metrics from GitHub,
Jira, and CI/CD pipelines into a single dashboard. Used by
engineering teams to track velocity.
</p>
<ul class="project-tech">
<li>Python</li>
<li>FastAPI</li>
<li>D3.js</li>
<li>Docker</li>
</ul>
<div class="project-links">
<a href="https://github.com/jane/devmetrics" target="_blank" rel="noopener noreferrer">GitHub</a>
</div>
</div>
</article>
<article class="project-card">
<div class="project-image">
<img src="images/project-3.jpg" alt="MarkdownPad editor screenshot" loading="lazy">
</div>
<div class="project-content">
<h3 class="project-name">MarkdownPad</h3>
<p class="project-description">
A distraction-free markdown editor with live preview, syntax
highlighting, and export to PDF/HTML. Built as a desktop app
with Electron.
</p>
<ul class="project-tech">
<li>TypeScript</li>
<li>Electron</li>
<li>CodeMirror</li>
</ul>
<div class="project-links">
<a href="https://github.com/jane/markdownpad" target="_blank" rel="noopener noreferrer">GitHub</a>
<a href="https://markdownpad.app" target="_blank" rel="noopener noreferrer">Download</a>
</div>
</div>
</article>
</div>
</div>
</section>
<!-- Skills Section -->
<section class="section" id="skills">
<div class="container">
<h2 class="section-title">Skills & Tools</h2>
<div class="skills-grid">
<div class="skill-category">
<h3>Frontend</h3>
<ul>
<li>JavaScript / TypeScript</li>
<li>React / Next.js</li>
<li>HTML / CSS</li>
<li>Tailwind CSS</li>
</ul>
</div>
<div class="skill-category">
<h3>Backend</h3>
<ul>
<li>Node.js / Express</li>
<li>Python / FastAPI</li>
<li>PostgreSQL / MongoDB</li>
<li>REST / GraphQL</li>
</ul>
</div>
<div class="skill-category">
<h3>Tools & Infrastructure</h3>
<ul>
<li>Git / GitHub</li>
<li>Docker</li>
<li>AWS / Vercel</li>
<li>CI/CD (GitHub Actions)</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Contact Section -->
<section class="section section-alt" id="contact">
<div class="container">
<h2 class="section-title">Get in Touch</h2>
<p class="contact-intro">
I'm currently open to new opportunities. Whether you have a
question, a project idea, or just want to say hi, my inbox is
always open.
</p>
<form class="contact-form" id="contactForm" action="https://formspree.io/f/your-form-id" method="POST">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
</form>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<div class="footer-links">
<a href="https://github.com/janedeveloper" target="_blank" rel="noopener noreferrer">GitHub</a>
<a href="https://linkedin.com/in/janedeveloper" target="_blank" rel="noopener noreferrer">LinkedIn</a>
<a href="https://twitter.com/janedeveloper" target="_blank" rel="noopener noreferrer">Twitter</a>
</div>
<p class="footer-credit">Built by Jane Developer</p>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>
Key structural decisions:
- Semantic HTML elements (
header,main,section,article,nav,footer) idattributes on sections for anchor navigationloading="lazy"on images below the foldrel="noopener noreferrer"on external links (security best practice)- Form uses Formspree for serverless form handling (free tier available)
CSS: Responsive and Clean
/ style.css /
/ ==================== Reset & Variables ==================== /
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--color-bg: #ffffff;
--color-bg-alt: #f8f9fa;
--color-text: #1a1a2e;
--color-text-secondary: #555555;
--color-accent: #2563eb;
--color-accent-hover: #1d4ed8;
--color-border: #e2e8f0;
--color-card-bg: #ffffff;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, sans-serif;
--font-mono: "SF Mono", "Fira Code", "Fira Mono", Menlo, monospace;
--max-width: 1100px;
--transition: 0.3s ease;
}
[data-theme="dark"] {
--color-bg: #0f172a;
--color-bg-alt: #1e293b;
--color-text: #e2e8f0;
--color-text-secondary: #94a3b8;
--color-accent: #60a5fa;
--color-accent-hover: #93bbfd;
--color-border: #334155;
--color-card-bg: #1e293b;
}
html {
scroll-behavior: smooth;
scroll-padding-top: 80px;
}
body {
font-family: var(--font-sans);
background-color: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
transition: background-color var(--transition), color var(--transition);
}
a {
color: var(--color-accent);
text-decoration: none;
}
a:hover {
color: var(--color-accent-hover);
}
img {
max-width: 100%;
display: block;
}
.container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 1.5rem;
}
/ ==================== Navigation ==================== /
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: var(--color-bg);
border-bottom: 1px solid var(--color-border);
transition: background-color var(--transition);
}
.nav {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 1.5rem;
height: 70px;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text);
}
.nav-links {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-links a {
color: var(--color-text-secondary);
font-size: 0.95rem;
transition: color var(--transition);
}
.nav-links a:hover {
color: var(--color-accent);
}
.theme-toggle {
background: none;
border: 2px solid var(--color-border);
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: border-color var(--transition);
}
.theme-toggle-icon::before {
content: "☀";
font-size: 1.2rem;
}
[data-theme="dark"] .theme-toggle-icon::before {
content: "☾";
}
.mobile-menu-btn {
display: none;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 5px;
}
.mobile-menu-btn span {
display: block;
width: 25px;
height: 2px;
background-color: var(--color-text);
transition: transform var(--transition), opacity var(--transition);
}
/ ==================== Hero ==================== /
.hero {
min-height: 100vh;
display: flex;
align-items: center;
padding-top: 70px;
}
.hero-greeting {
color: var(--color-accent);
font-family: var(--font-mono);
font-size: 1rem;
margin-bottom: 1rem;
}
.hero-name {
font-size: clamp(2.5rem, 6vw, 4.5rem);
font-weight: 800;
line-height: 1.1;
margin-bottom: 0.5rem;
}
.hero-title {
font-size: clamp(1.5rem, 4vw, 3rem);
color: var(--color-text-secondary);
font-weight: 600;
margin-bottom: 1.5rem;
}
.hero-description {
max-width: 540px;
color: var(--color-text-secondary);
font-size: 1.1rem;
margin-bottom: 2rem;
}
.hero-cta {
display: flex;
gap: 1rem;
}
/ ==================== Buttons ==================== /
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
transition: all var(--transition);
cursor: pointer;
border: 2px solid transparent;
}
.btn-primary {
background-color: var(--color-accent);
color: #ffffff;
border-color: var(--color-accent);
}
.btn-primary:hover {
background-color: var(--color-accent-hover);
border-color: var(--color-accent-hover);
color: #ffffff;
}
.btn-secondary {
background-color: transparent;
color: var(--color-accent);
border-color: var(--color-accent);
}
.btn-secondary:hover {
background-color: var(--color-accent);
color: #ffffff;
}
/ ==================== Sections ==================== /
.section {
padding: 6rem 0;
}
.section-alt {
background-color: var(--color-bg-alt);
}
.section-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 3rem;
position: relative;
}
.section-title::after {
content: "";
display: block;
width: 60px;
height: 3px;
background-color: var(--color-accent);
margin-top: 0.5rem;
}
/ ==================== About ==================== /
.about-content {
display: grid;
grid-template-columns: 3fr 2fr;
gap: 3rem;
align-items: start;
}
.about-text p {
color: var(--color-text-secondary);
margin-bottom: 1rem;
}
.about-image img {
border-radius: 8px;
width: 100%;
aspect-ratio: 1;
object-fit: cover;
}
/ ==================== Projects ==================== /
.projects-grid {
display: grid;
gap: 2rem;
}
.project-card {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
background-color: var(--color-card-bg);
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
transition: transform var(--transition), box-shadow var(--transition);
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.project-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.project-content {
padding: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
}
.project-name {
font-size: 1.4rem;
margin-bottom: 0.75rem;
}
.project-description {
color: var(--color-text-secondary);
margin-bottom: 1rem;
font-size: 0.95rem;
}
.project-tech {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
list-style: none;
margin-bottom: 1.5rem;
}
.project-tech li {
font-family: var(--font-mono);
font-size: 0.8rem;
background-color: var(--color-bg-alt);
color: var(--color-accent);
padding: 0.25rem 0.75rem;
border-radius: 4px;
}
.project-links {
display: flex;
gap: 1rem;
}
.project-links a {
font-weight: 600;
font-size: 0.9rem;
}
/ ==================== Skills ==================== /
.skills-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
.skill-category h3 {
font-size: 1.1rem;
margin-bottom: 1rem;
color: var(--color-accent);
}
.skill-category ul {
list-style: none;
}
.skill-category li {
color: var(--color-text-secondary);
padding: 0.4rem 0;
font-size: 0.95rem;
}
/ ==================== Contact ==================== /
.contact-intro {
color: var(--color-text-secondary);
max-width: 600px;
margin-bottom: 2rem;
font-size: 1.1rem;
}
.contact-form {
max-width: 600px;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.95rem;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--color-border);
border-radius: 6px;
font-size: 1rem;
font-family: var(--font-sans);
background-color: var(--color-bg);
color: var(--color-text);
transition: border-color var(--transition);
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--color-accent);
}
/ ==================== Footer ==================== /
.footer {
padding: 3rem 0;
text-align: center;
border-top: 1px solid var(--color-border);
}
.footer-links {
display: flex;
justify-content: center;
gap: 2rem;
margin-bottom: 1rem;
}
.footer-links a {
color: var(--color-text-secondary);
}
.footer-credit {
color: var(--color-text-secondary);
font-size: 0.85rem;
}
/ ==================== Responsive ==================== /
@media (max-width: 768px) {
.nav-links {
display: none;
position: fixed;
top: 70px;
left: 0;
right: 0;
background-color: var(--color-bg);
flex-direction: column;
align-items: center;
padding: 2rem;
gap: 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.nav-links.active {
display: flex;
}
.mobile-menu-btn {
display: flex;
}
.hero-name {
font-size: 2.5rem;
}
.hero-cta {
flex-direction: column;
}
.about-content {
grid-template-columns: 1fr;
}
.about-image {
order: -1;
max-width: 300px;
}
.project-card {
grid-template-columns: 1fr;
}
.skills-grid {
grid-template-columns: 1fr;
gap: 2rem;
}
}
Highlights of this CSS:
- CSS custom properties for theming -- switch all colors by toggling
data-theme clamp()for fluid typography that scales between mobile and desktop- CSS Grid for layouts, Flexbox for alignment
scroll-behavior: smoothfor smooth anchor navigation- One breakpoint at 768px handles the mobile layout (you can add more if needed)
- No CSS framework dependency
JavaScript: Dark Mode, Mobile Menu, Smooth Interactions
// script.js
// ==================== Dark Mode Toggle ====================
const themeToggle = document.getElementById("themeToggle");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
function setTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
}
function getStoredTheme() {
const stored = localStorage.getItem("theme");
if (stored) return stored;
return prefersDark.matches ? "dark" : "light";
}
// Initialize theme
setTheme(getStoredTheme());
themeToggle.addEventListener("click", () => {
const current = document.documentElement.getAttribute("data-theme");
setTheme(current === "dark" ? "light" : "dark");
});
// Listen for OS theme changes
prefersDark.addEventListener("change", (e) => {
if (!localStorage.getItem("theme")) {
setTheme(e.matches ? "dark" : "light");
}
});
// ==================== Mobile Menu ====================
const mobileMenuBtn = document.getElementById("mobileMenuBtn");
const navLinks = document.querySelector(".nav-links");
mobileMenuBtn.addEventListener("click", () => {
navLinks.classList.toggle("active");
mobileMenuBtn.classList.toggle("active");
});
// Close menu when a link is clicked
navLinks.querySelectorAll("a").forEach((link) => {
link.addEventListener("click", () => {
navLinks.classList.remove("active");
mobileMenuBtn.classList.remove("active");
});
});
// Close menu when clicking outside
document.addEventListener("click", (e) => {
if (!e.target.closest(".nav") && navLinks.classList.contains("active")) {
navLinks.classList.remove("active");
mobileMenuBtn.classList.remove("active");
}
});
// ==================== Scroll Animations ====================
const observerOptions = {
threshold: 0.1,
rootMargin: "0px 0px -50px 0px",
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
observer.unobserve(entry.target);
}
});
}, observerOptions);
// Observe all sections and project cards
document.querySelectorAll(".section, .project-card").forEach((el) => {
el.classList.add("fade-in");
observer.observe(el);
});
// ==================== Active Nav Link ====================
const sections = document.querySelectorAll("section[id]");
function updateActiveNav() {
const scrollY = window.scrollY + 100;
sections.forEach((section) => {
const sectionTop = section.offsetTop;
const sectionHeight = section.offsetHeight;
const sectionId = section.getAttribute("id");
const link = document.querySelector(.nav-links a[href="#${sectionId}"]);
if (!link) return;
if (scrollY >= sectionTop && scrollY < sectionTop + sectionHeight) {
link.classList.add("active");
} else {
link.classList.remove("active");
}
});
}
window.addEventListener("scroll", updateActiveNav, { passive: true });
// ==================== Contact Form ====================
const contactForm = document.getElementById("contactForm");
contactForm.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(contactForm);
const submitBtn = contactForm.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.textContent = "Sending...";
submitBtn.disabled = true;
try {
const response = await fetch(contactForm.action, {
method: "POST",
body: formData,
headers: {
Accept: "application/json",
},
});
if (response.ok) {
submitBtn.textContent = "Sent!";
contactForm.reset();
setTimeout(() => {
submitBtn.textContent = originalText;
submitBtn.disabled = false;
}, 3000);
} else {
throw new Error("Form submission failed");
}
} catch (error) {
submitBtn.textContent = "Error - Try Again";
submitBtn.disabled = false;
setTimeout(() => {
submitBtn.textContent = originalText;
}, 3000);
}
});
Add the fade-in animation CSS:
/ Add to style.css /
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
.nav-links a.active {
color: var(--color-accent);
}
The JavaScript is minimal and purposeful:
- Dark mode respects the OS preference, persists the user's choice, and toggles with a button
- Mobile menu opens/closes and auto-closes on link click or outside click
- Scroll animations use
IntersectionObserver(efficient, no scroll event spam) - Active nav highlights the current section as you scroll
- Contact form handles submission with loading/success/error states
Deploying to GitHub Pages
- Create a repository on GitHub (e.g.,
janedeveloper.github.iofor a user site, or any name for a project site)
- Push your code:
git init
git add .
git commit -m "Initial portfolio"
git branch -M main
git remote add origin https://github.com/janedeveloper/janedeveloper.github.io.git
git push -u origin main
- Go to your repository's Settings > Pages
- Under "Source," select "Deploy from a branch" and choose
main// (root)
- Your site will be live at
https://janedeveloper.github.iowithin a few minutes
- Add a
CNAMEfile to your repo root containing your domain:janedeveloper.com - Configure DNS at your registrar:
janedeveloper.github.io
- Enable "Enforce HTTPS" in the Pages settings once DNS propagates
Making It Stand Out
Most portfolios look the same because they follow the same template. Here's how to differentiate:
Show real projects, not tutorials. "Todo App" and "Weather App" signal that you followed a tutorial. Build something that solves a problem you actually had. Even a small utility that does one thing well is more impressive than a generic clone. Write good descriptions. Don't just list technologies. Explain what the project does, why you built it, and what you learned. "Built with React" tells me nothing. "Real-time collaborative kanban board handling 50+ concurrent users" tells me everything. Keep it fast. Test your site with Lighthouse. Aim for 95+ on Performance. Compress images (WebP format, appropriate dimensions). Your portfolio is the first impression of your technical skills -- if it takes 5 seconds to load, that's a statement. Use real content. Placeholder text and stock photos are obvious. Use your actual photo, your real projects, your genuine bio. Authenticity stands out more than any fancy animation. Don't over-animate. Subtle fade-ins as you scroll are fine. Parallax backgrounds, floating particles, and text that types itself out letter by letter are distracting and slow. Your work should be the focus, not the wrapper.Common Mistakes
Making the entire site a Single Page Application. This is a portfolio, not a web app. HTML, CSS, and a small script file load faster and rank better than a React app that needs to download, parse, and execute JavaScript before showing content. Listing every technology you've ever touched. Be honest. If you used Redis once in a tutorial, don't list it. Five technologies you know deeply are more impressive than twenty you've barely used. No mobile testing. Recruiters often browse on their phones. Test on actual devices, not just browser dev tools. The touch targets, font sizes, and layout all matter. Broken links. Test every link. Dead demo links and 404 GitHub repos look unprofessional. If a project demo is down, either fix it or remove the link. No contact information. The whole point of a portfolio is to be contacted. Make it obvious how to reach you. Email, LinkedIn, whatever you prefer -- just make it findable.What's Next
Your portfolio is a living document. Update it when you:
- Complete a significant project
- Learn a new skill you're confident in
- Change roles or career direction
- Get feedback from someone who hired developers
For more web development tutorials and career guides, check out CodeUp.