Building a Dynamic JS Portfolio at Parthh.in
A complete tutorial on building a dynamic JavaScript portfolio website. Covers project structure, responsive design with CSS Grid and Flexbox, animated project cards, dark/light theme toggle, contact form with Fetch API submission, SEO metadata, performance optimization, and deploying to a custom domain.
A portfolio website showcases your projects, skills, and personality to potential employers and collaborators. This guide builds a dynamic, responsive portfolio using vanilla JavaScript with animated project cards, theme switching, a contact form powered by the Fetch API, and deployment-ready optimization.
Project Structure
portfolio/
index.html
css/
styles.css
themes.css
js/
main.js
projects.js
theme.js
contact.js
assets/
images/
icons/
data/
projects.json
Keeping JavaScript in separate modules makes the code maintainable. See JavaScript ES6 modules import export guide for module organization patterns.
HTML Foundation
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Parth's portfolio - JavaScript developer specializing in web applications">
<title>Parth | JavaScript Developer</title>
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="css/themes.css">
</head>
<body>
<header class="site-header">
<nav class="nav-container">
<a href="#" class="logo">Parth.dev</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 id="theme-toggle" aria-label="Toggle theme">
<span class="theme-icon"></span>
</button>
</nav>
</header>
<main>
<section id="hero" class="hero-section">
<h1>Hi, I'm <span class="highlight">Parth</span></h1>
<p class="tagline">I build things for the web with JavaScript</p>
</section>
<section id="projects" class="projects-section">
<h2>Projects</h2>
<div class="filter-bar">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="frontend">Frontend</button>
<button class="filter-btn" data-filter="fullstack">Full Stack</button>
<button class="filter-btn" data-filter="tool">Tools</button>
</div>
<div id="project-grid" class="project-grid"></div>
</section>
<section id="skills" class="skills-section">
<h2>Skills</h2>
<div id="skills-grid" class="skills-grid"></div>
</section>
<section id="contact" class="contact-section">
<h2>Get In Touch</h2>
<form id="contact-form" class="contact-form">
<input type="text" name="name" placeholder="Your Name" required>
<input type="email" name="email" placeholder="Your Email" required>
<textarea name="message" placeholder="Your Message" rows="5" required></textarea>
<button type="submit" class="submit-btn">Send Message</button>
<p id="form-status" class="form-status"></p>
</form>
</section>
</main>
<script type="module" src="js/main.js"></script>
</body>
</html>Project Data
Store projects as JSON for easy updates:
// data/projects.json
[
{
"id": 1,
"title": "Task Manager App",
"description": "A full-stack task management application with drag-and-drop Kanban board",
"tags": ["JavaScript", "Node.js", "MongoDB"],
"category": "fullstack",
"image": "assets/images/task-manager.webp",
"liveUrl": "https://tasks.parthh.in",
"repoUrl": "https://github.com/parth/task-manager"
},
{
"id": 2,
"title": "Weather Dashboard",
"description": "Real-time weather data with charts and 5-day forecast",
"tags": ["JavaScript", "Fetch API", "Chart.js"],
"category": "frontend",
"image": "assets/images/weather.webp",
"liveUrl": "https://weather.parthh.in",
"repoUrl": "https://github.com/parth/weather-dashboard"
}
]Dynamic Project Cards
// js/projects.js
export async function loadProjects() {
const response = await fetch("data/projects.json");
if (!response.ok) throw new Error("Failed to load projects");
return response.json();
}
export function renderProjectCard(project) {
const card = document.createElement("article");
card.className = "project-card";
card.dataset.category = project.category;
card.innerHTML = `
<div class="card-image">
<img src="${project.image}" alt="${project.title}" loading="lazy" width="400" height="250">
</div>
<div class="card-content">
<h3>${project.title}</h3>
<p>${project.description}</p>
<div class="card-tags">
${project.tags.map(tag => `<span class="tag">${tag}</span>`).join("")}
</div>
<div class="card-links">
<a href="${project.liveUrl}" target="_blank" rel="noopener noreferrer">Live Demo</a>
<a href="${project.repoUrl}" target="_blank" rel="noopener noreferrer">Source Code</a>
</div>
</div>
`;
return card;
}
export function renderProjectGrid(projects, container) {
container.innerHTML = "";
projects.forEach(project => {
container.appendChild(renderProjectCard(project));
});
}Project Filtering
// js/projects.js (continued)
export function setupFilters(projects, container) {
const buttons = document.querySelectorAll(".filter-btn");
buttons.forEach(btn => {
btn.addEventListener("click", () => {
// Update active button
buttons.forEach(b => b.classList.remove("active"));
btn.classList.add("active");
// Filter projects
const filter = btn.dataset.filter;
const filtered = filter === "all"
? projects
: projects.filter(p => p.category === filter);
renderProjectGrid(filtered, container);
// Animate cards in
container.querySelectorAll(".project-card").forEach((card, i) => {
card.style.animationDelay = `${i * 0.1}s`;
card.classList.add("fade-in");
});
});
});
}Theme Toggle
// js/theme.js
export function setupThemeToggle() {
const toggle = document.getElementById("theme-toggle");
const root = document.documentElement;
// Load saved preference
const saved = localStorage.getItem("theme");
if (saved) {
root.dataset.theme = saved;
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
root.dataset.theme = "dark";
}
toggle.addEventListener("click", () => {
const current = root.dataset.theme;
const next = current === "dark" ? "light" : "dark";
root.dataset.theme = next;
localStorage.setItem("theme", next);
});
}Theme CSS
/* css/themes.css */
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f8fafc;
--text-primary: #1e293b;
--text-secondary: #64748b;
--accent: #3b82f6;
--card-bg: #ffffff;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--accent: #60a5fa;
--card-bg: #1e293b;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
}Contact Form With Fetch
// js/contact.js
export function setupContactForm() {
const form = document.getElementById("contact-form");
const status = document.getElementById("form-status");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const submitBtn = form.querySelector(".submit-btn");
submitBtn.disabled = true;
submitBtn.textContent = "Sending...";
status.textContent = "";
const formData = new FormData(form);
const data = Object.fromEntries(formData);
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error("Failed to send message");
status.textContent = "Message sent! I will get back to you soon.";
status.className = "form-status success";
form.reset();
} catch (error) {
status.textContent = "Something went wrong. Please try again.";
status.className = "form-status error";
} finally {
submitBtn.disabled = false;
submitBtn.textContent = "Send Message";
}
});
}See handling POST requests with JS fetch API guide for more on form submission patterns.
Scroll Animations With Intersection Observer
// js/main.js
function setupScrollAnimations() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("animate-in");
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1, rootMargin: "0px 0px -50px 0px" }
);
document.querySelectorAll("section").forEach(section => {
observer.observe(section);
});
}Main Entry Point
// js/main.js
import { loadProjects, renderProjectGrid, setupFilters } from "./projects.js";
import { setupThemeToggle } from "./theme.js";
import { setupContactForm } from "./contact.js";
async function init() {
setupThemeToggle();
setupContactForm();
setupScrollAnimations();
const container = document.getElementById("project-grid");
const projects = await loadProjects();
renderProjectGrid(projects, container);
setupFilters(projects, container);
}
init();Performance Checklist
| Optimization | Implementation |
|---|---|
| Lazy load images | loading="lazy" attribute |
| Optimize images | WebP format, proper sizing |
| Minify CSS/JS | Build tool or CDN |
| Defer non-critical JS | <script type="module"> (auto-deferred) |
| Preload hero font | <link rel="preload" as="font"> |
| Cache static assets | Service worker or CDN headers |
Rune AI
Key Insights
- Separate data from presentation: Store projects in JSON and render dynamically; adding a project requires editing JSON, not HTML
- Theme toggle with localStorage: Save preference and respect
prefers-color-schememedia query as default - Intersection Observer for scroll animation: More efficient than scroll event listeners; use
unobserveafter animation triggers - Progressive enhancement: The site works without JavaScript (semantic HTML), then JS adds interactivity
- Performance matters for portfolios: Lazy-load images, use WebP, defer scripts; a slow portfolio reflects poorly on your engineering skills
Frequently Asked Questions
Should I use a framework for a portfolio?
How do I handle the contact form without a backend?
What is the best way to deploy a static portfolio?
How many projects should I showcase?
Conclusion
A dynamic JavaScript portfolio loads project data from JSON, renders cards with filtering, supports theme switching via localStorage, submits a contact form via the Fetch API, and animates on scroll with Intersection Observer. The key is clean vanilla JavaScript that demonstrates your ability to ship polished, performant web experiences. For the Web APIs used throughout this project, see browser Web APIs in JavaScript complete guide. For the module system organizing the code, see JavaScript named exports a complete tutorial.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.