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.

JavaScriptintermediate
14 min read

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

CodeCode
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

htmlhtml
<!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:

javascriptjavascript
// 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

javascriptjavascript
// 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

javascriptjavascript
// 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

javascriptjavascript
// 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

csscss
/* 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

javascriptjavascript
// 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

javascriptjavascript
// 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

javascriptjavascript
// 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

OptimizationImplementation
Lazy load imagesloading="lazy" attribute
Optimize imagesWebP format, proper sizing
Minify CSS/JSBuild tool or CDN
Defer non-critical JS<script type="module"> (auto-deferred)
Preload hero font<link rel="preload" as="font">
Cache static assetsService worker or CDN headers
Rune AI

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-scheme media query as default
  • Intersection Observer for scroll animation: More efficient than scroll event listeners; use unobserve after 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
RunePowered by Rune AI

Frequently Asked Questions

Should I use a framework for a portfolio?

For a simple portfolio, vanilla JavaScript is often better. It loads faster, has no dependencies, and demonstrates core JS skills. Use a framework if you want to showcase framework-specific skills.

How do I handle the contact form without a backend?

Use a form service like Formspree, Netlify Forms, or EmailJS. They provide an endpoint that forwards form submissions to your email.

What is the best way to deploy a static portfolio?

GitHub Pages (free), Vercel, or Netlify. All support custom domains and HTTPS. See [deploying JS modules using the GitHub Student plan](/tutorials/programming-languages/javascript/deploying-js-modules-using-the-github-student-plan) for student-specific options.

How many projects should I showcase?

4-6 high-quality projects is ideal. Quality over quantity. Include a mix of project types (frontend, full-stack, tool) to show range.

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.