Build a Portfolio Website

Build and deploy a responsive portfolio site to GitHub Pages.

Introduction

30 Second Summary

Every project you complete is proof that you can build real things. But without a single place to showcase your work, that proof stays scattered across folders and forgotten repositories.

In this project, you will build a professional portfolio website from scratch using HTML, CSS, and JavaScript, then deploy it live to GitHub Pages so anyone with the link can see what you have built.

What You'll Build

A responsive portfolio website featuring a hero section, a filterable project showcase grid, smooth scroll animations, and a live deployment connected to your Git workflow.

By the end of this project, you'll have:

  • A complete portfolio website built with vanilla HTML, CSS, and JavaScript that looks professional on any screen size.
  • A filterable project showcase using CSS Grid and JavaScript event listeners so visitors can browse your work by category.
  • A live deployment at yourusername.github.io that updates with a single git push.
  • Secret Mission: Add a dark/light theme toggle that remembers your visitor's preference.

Are there any prerequisites?

You should have basic familiarity with HTML, CSS, and JavaScript. You do not need to be an expert. If you have written a few web pages before, you are ready.

Before We Start

You're building a portfolio website to present your completed projects to potential employers, clients, and collaborators. A live portfolio proves you can ship real work, not just talk about it.

Before you start building, you need three things ready: Git for version control, a GitHub repository for hosting, and your project files set up in VS Code.

In this step, get ready to:
  • Verify Git is installed on your machine.
  • Create a GitHub repository for your portfolio site.
  • Set up your project files in VS Code.
Open VS Code and your terminal
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open VS Code's integrated terminal by clicking Terminal in the top menu bar, then clicking New Terminal.

Terminal shortcut

You can also toggle the terminal panel with Ctrl+` (backtick key) on both macOS and Windows.

Verify Git is installed

Git tracks changes to your code and lets you push updates to GitHub. Let's confirm it's installed.

  • Check your Git installation by running this command in your terminal:
git --version

✔️ I see a version number

You're all set. Any version number (e.g. git version 2.39.0) means Git is installed and ready to use.

ⓧ Command not found

Git is not installed yet. Follow these steps to install it.

  • Go to https://git-scm.com/downloads in your browser.
  • Download the installer for your operating system.
  • Run the installer and accept all default settings.
  • Close and reopen VS Code so the terminal picks up the new installation.
  • Run git --version again to confirm it works.

Still not working after install?

On Windows, make sure you selected the option to add Git to your PATH during installation. If you missed it, re-run the installer and look for the "Adjusting your PATH environment" step.

Help me fix my PATH so Git is recognized

Create your GitHub repository

GitHub Pages hosts your website for free directly from a repository. The repository name determines your site's URL.

  • Go to github.com and sign in to your account.
  • Click the + icon in the top-right corner, then click New repository.
  • In the Repository name field, enter your GitHub username followed by .github.io (for example, janedoe.github.io).
  • Make sure Public is selected. GitHub Pages requires a public repository on the free plan.
  • Check the box for Add a README file.
  • Click Create repository.

Why this exact name?

GitHub Pages uses the naming convention username.github.io to identify your user site. This is what makes your portfolio accessible at https://yourusername.github.io.

You can only create one user site per GitHub account.

Now clone the repository to your computer so you can work on it locally.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone your new repository by running this command:
git clone https://github.com/[[GITHUB_USERNAME="your GitHub username"]]/[[GITHUB_USERNAME="your GitHub username"]].github.io.git

What does this command do?

git clone downloads a copy of your GitHub repository to your computer. It creates a new folder on your Desktop with the same name as your repository.

  • Move into your cloned repository folder by running:
cd [[GITHUB_USERNAME="your GitHub username"]].github.io

Seeing 'repository not found'?

Double-check that you typed your exact GitHub username in the clone URL. The username is case-sensitive. Also confirm you created the repository as Public, not Private.

Help me fix a 'repository not found' error when cloning

Set up your project files

Your portfolio needs three files: one for structure (HTML), one for styling (CSS), and one for interactivity (JavaScript).

  • Create the three project files by running this command:

macOS

touch index.html style.css script.js

Windows

New-Item index.html, style.css, script.js
  • Delete the README.md file by running:

macOS

rm README.md

Windows

Remove-Item README.md

Why delete README.md?

GitHub Pages looks for an index.html file as the entry point for your site. Removing README.md avoids any confusion and keeps your repository clean.

  • Confirm the files exist by running:
ls

You should see index.html, style.css, and script.js in the output.

  • Now open this folder in VS Code. Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the yourusername.github.io folder (the one you just cloned).
  • Click Open.

You should see your three files listed in the Explorer sidebar on the left.

Don't see your files in the Explorer?

Make sure you opened the .github.io folder itself, not the Desktop folder containing it. The Explorer should show index.html, style.css, and script.js at the top level.

Help me open the correct folder in VS Code

Your environment is ready. Next up, you'll build the HTML structure for your portfolio site.

Build the HTML Structure

With your project files in place, it's time to build the actual content of your portfolio.

Every website starts with HTML that defines its structure and content. In this step, you'll create the skeleton of your portfolio page using semantic elements that describe the purpose of each section.

In this step, get ready to:
  • Build the page head, navigation bar, and hero section.
  • Create a project showcase grid with filterable project cards.
  • Add an about section, contact footer, and link your CSS and JS files.
Add the document head and navigation

The document head sets metadata (character encoding, viewport, page title) and links your stylesheet. The navigation bar gives visitors a way to jump between sections.

  • In VS Code, click on index.html in the Explorer sidebar to open it.
  • Copy and paste the following code into the empty file:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Your Name - Portfolio</title>
    <!-- Link the stylesheet so CSS applies to this page -->
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- Fixed navigation bar at the top -->
    <header>
        <nav class="navbar">
            <a href="#" class="logo">YourName</a>
            <ul class="nav-links">
                <li><a href="#projects">Projects</a></li>
                <li><a href="#about">About</a></li>
                <li><a href="#contact">Contact</a></li>
            </ul>
        </nav>
    </header>

    <main>
        <!-- Hero section: your name and tagline -->
        <section class="hero" id="hero">
            <h1>Hi, I'm Your Name</h1>
            <p class="tagline">I build things for the cloud and the web.</p>
        </section>

What does this code do?

The <meta name="viewport"> tag tells mobile browsers to scale the page to the device width. Without it, your site would appear zoomed out on phones.

The <nav> links use #projects, #about, and #contact as anchors. These correspond to the id attributes you'll add to each section so the browser knows where to scroll.

  • Save index.html with Cmd+S (macOS) or Ctrl+S (Windows).
  • Open the file in your browser by right-clicking index.html in the Explorer sidebar, selecting Reveal in Finder (macOS) or Reveal in File Explorer (Windows), then double-clicking the file.
  • You should see your name, tagline, and three navigation links (Projects, About, Contact) at the top of the page in plain unstyled text.

Seeing a blank page?

Make sure you saved the file after pasting. Check that the file is named index.html (not index.html.txt). If your browser shows the raw code instead of rendered content, make sure you opened the file by double-clicking it in Finder or File Explorer, not by dragging it into VS Code.

Help me debug my blank HTML page.

Build the projects section with filter cards

The projects section is the centrepiece of your portfolio. Each project card carries a data-category attribute that you'll later use with JavaScript to filter cards by category.

  • In index.html, place your cursor directly after the closing </section> tag of the hero section (the last line of code you pasted).
  • Paste the following code on the next line:
        <section class="projects" id="projects">
            <h2>My Projects</h2>
            <!-- Filter buttons for sorting project cards -->
            <div class="filter-bar">
                <button class="filter-btn active" data-filter="all">All</button>
                <button class="filter-btn" data-filter="cloud">Cloud</button>
                <button class="filter-btn" data-filter="web">Web</button>
                <button class="filter-btn" data-filter="data">Data</button>
            </div>
            <!-- Grid of project cards -->
            <div class="project-grid">
                <article class="project-card hidden" data-category="cloud">
                    <div class="card-image">
                        <img src="https://placehold.co/400x200/1a1a2e/eee?text=Cloud+Project" alt="Cloud project screenshot">
                    </div>
                    <div class="card-content">
                        <h3>Cloud Infrastructure</h3>
                        <p>Deployed a scalable web application using AWS services including S3, Lambda, and API Gateway.</p>
                        <div class="tech-tags">
                            <span>AWS</span>
                            <span>Terraform</span>
                            <span>Python</span>
                        </div>
                        <a href="#" class="card-link">View Project</a>
                    </div>
                </article>

                <article class="project-card hidden" data-category="web">
                    <div class="card-image">
                        <img src="https://placehold.co/400x200/16213e/eee?text=Web+Project" alt="Web project screenshot">
                    </div>
                    <div class="card-content">
                        <h3>Interactive Dashboard</h3>
                        <p>Built a real-time dashboard with data visualization and responsive design for monitoring metrics.</p>
                        <div class="tech-tags">
                            <span>JavaScript</span>
                            <span>HTML/CSS</span>
                            <span>Chart.js</span>
                        </div>
                        <a href="#" class="card-link">View Project</a>
                    </div>
                </article>

                <article class="project-card hidden" data-category="data">
                    <div class="card-image">
                        <img src="https://placehold.co/400x200/0f3460/eee?text=Data+Project" alt="Data project screenshot">
                    </div>
                    <div class="card-content">
                        <h3>Data Pipeline</h3>
                        <p>Designed an ETL pipeline to process and analyze large datasets with automated reporting.</p>
                        <div class="tech-tags">
                            <span>Python</span>
                            <span>SQL</span>
                            <span>Pandas</span>
                        </div>
                        <a href="#" class="card-link">View Project</a>
                    </div>
                </article>
            </div>
        </section>

What does this code do?

Each filter button has a data-filter attribute (like data-filter="cloud") and each project card has a matching data-category attribute (like data-category="cloud"). Your JavaScript will compare these later to show or hide cards.

The hidden class on each card is for a scroll animation you'll add later. Cards start invisible and fade in as you scroll down the page.

  • Save index.html and refresh your browser.
  • You should see a "My Projects" heading, four filter buttons (All, Cloud, Web, Data), and three project cards with titles, descriptions, technology tags, and placeholder images.

Don't see the project cards?

Check that you pasted the code after the hero section's closing </section> tag, not inside it. If you see broken-looking text, look for any missing closing tags or misplaced angle brackets in your code.

Help me figure out why my project cards aren't showing.

Add the about section and footer

The final pieces are an about section for your personal story and a footer with contact links. You'll also close out the <main> element and link your JavaScript file.

  • In index.html, place your cursor directly after the closing </section> tag of the projects section (the last line you pasted).
  • Paste the following code on the next line:
        <!-- About section with scroll animation -->
        <section class="about hidden" id="about">
            <h2>About Me</h2>
            <p>I'm a developer passionate about building solutions that make a difference. I enjoy working across the full stack, from cloud infrastructure to frontend interfaces. When I'm not coding, you'll find me learning new technologies and contributing to open source.</p>
        </section>

        <!-- Footer with contact links -->
        <footer id="contact">
            <h2>Get In Touch</h2>
            <div class="contact-links">
                <a href="https://github.com/yourusername" target="_blank">GitHub</a>
                <a href="https://linkedin.com/in/yourprofile" target="_blank">LinkedIn</a>
                <a href="mailto:your@email.com">Email</a>
            </div>
            <p class="copyright">Built by Your Name</p>
        </footer>
    </main>

    <!-- Link JavaScript file before closing body -->
    <script src="script.js"></script>
</body>
</html>

What does this code do?

The <footer> uses id="contact" so the "Contact" link in your navigation scrolls directly to it.

The <script> tag is placed just before </body> so the HTML loads first. This guarantees that when your JavaScript runs, all the elements it needs to interact with already exist on the page.

  • Save index.html and refresh your browser.
  • You should now see the full page: navigation links at the top, your name and tagline, project cards with placeholder images, an "About Me" paragraph, and contact links at the bottom.
  • Click the Projects, About, and Contact links in the navigation. Each should jump the page to the matching section.

Navigation links not jumping to sections?

Each href="#projects" in the nav must match an id="projects" on a section element exactly. Check for typos or mismatched capitalization between your href values and id attributes.

Help me fix my navigation anchor links.

What are semantic HTML elements?

Elements like <header>, <nav>, <main>, <section>, <article>, and <footer> describe the purpose of their content, not just its appearance. Search engines and screen readers use them to understand your page's structure.

A <div> says "this is a box." A <section> says "this is a distinct topic." Same visual result, but much more meaning for machines reading your code.

✔️ Awesome, I've got everything!

Great. Double check you've saved index.html and that clicking each nav link scrolls to the correct section.

ⓧ I'd like to double check the full code

Here is the complete index.html file. Make sure your code matches this exactly:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Your Name - Portfolio</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <nav class="navbar">
            <a href="#" class="logo">YourName</a>
            <ul class="nav-links">
                <li><a href="#projects">Projects</a></li>
                <li><a href="#about">About</a></li>
                <li><a href="#contact">Contact</a></li>
            </ul>
        </nav>
    </header>

    <main>
        <section class="hero" id="hero">
            <h1>Hi, I'm Your Name</h1>
            <p class="tagline">I build things for the cloud and the web.</p>
        </section>

        <section class="projects" id="projects">
            <h2>My Projects</h2>
            <div class="filter-bar">
                <button class="filter-btn active" data-filter="all">All</button>
                <button class="filter-btn" data-filter="cloud">Cloud</button>
                <button class="filter-btn" data-filter="web">Web</button>
                <button class="filter-btn" data-filter="data">Data</button>
            </div>
            <div class="project-grid">
                <article class="project-card hidden" data-category="cloud">
                    <div class="card-image">
                        <img src="https://placehold.co/400x200/1a1a2e/eee?text=Cloud+Project" alt="Cloud project screenshot">
                    </div>
                    <div class="card-content">
                        <h3>Cloud Infrastructure</h3>
                        <p>Deployed a scalable web application using AWS services including S3, Lambda, and API Gateway.</p>
                        <div class="tech-tags">
                            <span>AWS</span>
                            <span>Terraform</span>
                            <span>Python</span>
                        </div>
                        <a href="#" class="card-link">View Project</a>
                    </div>
                </article>

                <article class="project-card hidden" data-category="web">
                    <div class="card-image">
                        <img src="https://placehold.co/400x200/16213e/eee?text=Web+Project" alt="Web project screenshot">
                    </div>
                    <div class="card-content">
                        <h3>Interactive Dashboard</h3>
                        <p>Built a real-time dashboard with data visualization and responsive design for monitoring metrics.</p>
                        <div class="tech-tags">
                            <span>JavaScript</span>
                            <span>HTML/CSS</span>
                            <span>Chart.js</span>
                        </div>
                        <a href="#" class="card-link">View Project</a>
                    </div>
                </article>

                <article class="project-card hidden" data-category="data">
                    <div class="card-image">
                        <img src="https://placehold.co/400x200/0f3460/eee?text=Data+Project" alt="Data project screenshot">
                    </div>
                    <div class="card-content">
                        <h3>Data Pipeline</h3>
                        <p>Designed an ETL pipeline to process and analyze large datasets with automated reporting.</p>
                        <div class="tech-tags">
                            <span>Python</span>
                            <span>SQL</span>
                            <span>Pandas</span>
                        </div>
                        <a href="#" class="card-link">View Project</a>
                    </div>
                </article>
            </div>
        </section>

        <section class="about hidden" id="about">
            <h2>About Me</h2>
            <p>I'm a developer passionate about building solutions that make a difference. I enjoy working across the full stack, from cloud infrastructure to frontend interfaces. When I'm not coding, you'll find me learning new technologies and contributing to open source.</p>
        </section>

        <footer id="contact">
            <h2>Get In Touch</h2>
            <div class="contact-links">
                <a href="https://github.com/yourusername" target="_blank">GitHub</a>
                <a href="https://linkedin.com/in/yourprofile" target="_blank">LinkedIn</a>
                <a href="mailto:your@email.com">Email</a>
            </div>
            <p class="copyright">Built by Your Name</p>
        </footer>
    </main>

    <script src="script.js"></script>
</body>
</html>

Your HTML structure is complete. All the content is on the page, but it looks plain and unstyled. Next up, you'll use CSS to transform this raw structure into a polished, responsive design with a professional color scheme and card layout.

Style with CSS

Your HTML is structured and semantic, but it looks like a plain document from the early internet. A professional portfolio needs to look polished and intentional to make a strong first impression.

In this step, you will transform your raw HTML into a visually appealing layout using CSS custom properties, a responsive CSS Grid for project cards, and media queries that adapt to any screen size.

In this step, get ready to:
  • Define a design system with CSS custom properties for colors, fonts, and spacing.
  • Build a responsive project grid with card hover effects.
  • Add media queries so your site looks great on tablets and phones.
Set up your design system and navigation styles

CSS custom properties let you define your entire color palette and spacing scale in one place. When you want to tweak the look later, you change one variable instead of hunting through hundreds of lines.

  • In VS Code, open style.css from the Explorer sidebar.
  • Add your design tokens, CSS reset, and base body styles by pasting the following code:
:root {
    --color-bg: #f8f9fa;
    --color-text: #1a1a2e;
    --color-heading: #16213e;
    --color-accent: #0f3460;
    --color-card-bg: #ffffff;
    --color-card-shadow: rgba(0, 0, 0, 0.1);
    --color-nav-bg: #ffffff;
    --color-tag-bg: #e8edf3;
    --font-size-base: 1rem;
    --font-size-lg: 1.25rem;
    --font-size-xl: 2.5rem;
    --spacing-sm: 0.5rem;
    --spacing-md: 1rem;
    --spacing-lg: 2rem;
    --spacing-xl: 4rem;
    --border-radius: 8px;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html {
    scroll-behavior: smooth;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background-color: var(--color-bg);
    color: var(--color-text);
    line-height: 1.6;
}

What does this code do?

The :root block defines CSS custom properties (variables) that the rest of your stylesheet references with var(--name). This creates a single source of truth for your design.

The * selector removes default browser margins and padding so every element starts from a clean slate. scroll-behavior: smooth on html makes anchor links glide to their target instead of jumping.

Now you need a fixed navigation bar that stays visible as the learner scrolls.

  • Add the navigation styles directly below the body block by pasting:
.navbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--spacing-md) var(--spacing-lg);
    background-color: var(--color-nav-bg);
    box-shadow: 0 2px 4px var(--color-card-shadow);
    z-index: 1000;
}

.logo {
    font-size: var(--font-size-lg);
    font-weight: 700;
    color: var(--color-accent);
    text-decoration: none;
}

.nav-links {
    display: flex;
    list-style: none;
    gap: var(--spacing-lg);
}

.nav-links a {
    text-decoration: none;
    color: var(--color-text);
    font-weight: 500;
    transition: color 0.3s ease;
}

.nav-links a:hover {
    color: var(--color-accent);
}

What does this code do?

The position: fixed pins the navbar to the top of the viewport so it stays visible while scrolling. z-index: 1000 ensures it sits above all other content.

Flexbox with justify-content: space-between pushes the logo to the left and the nav links to the right. The transition on link color creates a smooth hover effect.

  • Save style.css and open index.html in your browser (double-click the file in Finder or File Explorer).

You should see a white navigation bar pinned to the top of the page with your logo on the left and links on the right. The background is now light gray instead of white.

Page still looks unstyled?

Make sure your style.css file is in the same folder as index.html. The <link> tag in your HTML expects them side by side.

Also confirm you saved the file. Unsaved changes in VS Code show a dot on the file tab.

Why isn't my CSS file loading in the browser?

Style the hero section and project grid

The hero section is the first thing visitors see. It needs to fill the viewport and center your name prominently. After the hero, your project grid needs to adapt to different screen widths automatically.

  • Add the hero, projects section, and filter button styles below the .nav-links a:hover block by pasting:
.hero {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    min-height: 100vh;
    padding: var(--spacing-xl) var(--spacing-lg);
}

.hero h1 {
    font-size: var(--font-size-xl);
    color: var(--color-heading);
    margin-bottom: var(--spacing-md);
}

.tagline {
    font-size: var(--font-size-lg);
    color: var(--color-accent);
    opacity: 0.8;
}

.projects {
    padding: var(--spacing-xl) var(--spacing-lg);
    max-width: 1200px;
    margin: 0 auto;
}

.projects h2,
.about h2,
footer h2 {
    font-size: 2rem;
    color: var(--color-heading);
    margin-bottom: var(--spacing-lg);
    text-align: center;
}

.filter-bar {
    display: flex;
    justify-content: center;
    gap: var(--spacing-sm);
    margin-bottom: var(--spacing-lg);
    flex-wrap: wrap;
}

.filter-btn {
    padding: var(--spacing-sm) var(--spacing-md);
    border: 2px solid var(--color-accent);
    background: transparent;
    color: var(--color-accent);
    border-radius: 20px;
    cursor: pointer;
    font-weight: 500;
    transition: all 0.3s ease;
}

.filter-btn.active,
.filter-btn:hover {
    background-color: var(--color-accent);
    color: #ffffff;
}

What does this code do?

The .hero uses flexbox to vertically and horizontally center your name and tagline. min-height: 100vh makes it fill the full viewport height so it dominates the first screen.

The filter buttons use pill-shaped styling with border-radius: 20px. The .active and :hover states fill the button with your accent color so it is clear which filter is selected.

  • Save style.css and refresh your browser.

Your name and tagline now fill the entire first screen, centered vertically. Scrolling down reveals the "My Projects" heading with pill-shaped filter buttons below it.

Next, you will build the responsive project grid and style each card. This is the core visual feature of your portfolio.

  • Add the project grid and card styles below the .filter-btn.active block by pasting:
.project-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: var(--spacing-lg);
}

.project-card {
    background-color: var(--color-card-bg);
    border-radius: var(--border-radius);
    box-shadow: 0 4px 6px var(--color-card-shadow);
    overflow: hidden;
    transition: transform 0.3s ease, box-shadow 0.3s ease, opacity 0.4s ease;
}

.project-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 16px var(--color-card-shadow);
}

.project-card.card-hidden {
    display: none;
}

.card-image img {
    width: 100%;
    height: 200px;
    object-fit: cover;
}

.card-content {
    padding: var(--spacing-md);
}

.card-content h3 {
    color: var(--color-heading);
    margin-bottom: var(--spacing-sm);
}

.card-content p {
    margin-bottom: var(--spacing-md);
    color: var(--color-text);
    opacity: 0.85;
}

.tech-tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-sm);
    margin-bottom: var(--spacing-md);
}

.tech-tags span {
    background-color: var(--color-tag-bg);
    padding: 0.25rem 0.75rem;
    border-radius: 12px;
    font-size: 0.85rem;
    color: var(--color-accent);
}

.card-link {
    display: inline-block;
    color: var(--color-accent);
    font-weight: 600;
    text-decoration: none;
}

.card-link:hover {
    text-decoration: underline;
}

How does CSS Grid make this responsive?

The key line is grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)). It tells the browser to create as many columns as will fit, with each column at least 300px wide. On a wide screen you get 3 columns. On a narrow screen the cards stack into fewer columns automatically.

The transform: translateY(-4px) on hover lifts the card slightly, giving a tactile "floating" feeling that signals interactivity.

  • Save style.css and refresh your browser.

Your project cards now appear as white rounded rectangles arranged in a grid. Hover over a card to see it lift up with a deeper shadow.

Cards not appearing at all?

Remember that your project cards have a hidden class in the HTML. You have not added the .hidden CSS yet, so the cards may be invisible (opacity 0). You will add this in the next chunk. If you see nothing, keep going.

Help me figure out why my project cards are invisible.

Now finish the page layout with the about section, footer, and scroll animation classes.

  • Add the remaining section styles and animation classes below the .card-link:hover block by pasting:
.about {
    padding: var(--spacing-xl) var(--spacing-lg);
    max-width: 800px;
    margin: 0 auto;
    text-align: center;
}

.about p {
    font-size: var(--font-size-lg);
    line-height: 1.8;
}

footer {
    padding: var(--spacing-xl) var(--spacing-lg);
    text-align: center;
    background-color: var(--color-heading);
    color: #ffffff;
}

footer h2 {
    color: #ffffff;
}

.contact-links {
    display: flex;
    justify-content: center;
    gap: var(--spacing-lg);
    margin-bottom: var(--spacing-lg);
}

.contact-links a {
    color: #ffffff;
    text-decoration: none;
    font-weight: 500;
    padding: var(--spacing-sm) var(--spacing-md);
    border: 1px solid #ffffff;
    border-radius: 4px;
    transition: background-color 0.3s ease;
}

.contact-links a:hover {
    background-color: rgba(255, 255, 255, 0.1);
}

.copyright {
    opacity: 0.7;
    font-size: 0.9rem;
}

.hidden {
    opacity: 0;
    transform: translateY(20px);
    transition: opacity 0.6s ease, transform 0.6s ease;
}

.visible {
    opacity: 1;
    transform: translateY(0);
}

What are the hidden and visible classes for?

The .hidden class sets elements to transparent and shifted 20px downward. The .visible class brings them back to full opacity and their normal position.

In the next step, JavaScript will swap these classes as elements scroll into view, creating a smooth fade-in and slide-up animation. For now, elements with .hidden will be invisible until you add the JavaScript.

  • Save style.css and refresh your browser.

Scroll past the hero section. You should see the dark footer at the bottom with white contact links. The project cards and about section are invisible right now because they have the .hidden class. They will appear once you add JavaScript in the next step.

Add responsive breakpoints for tablet and mobile

Your site looks good at desktop width, but on smaller screens the grid and navigation need to adapt. Media queries let you override specific styles at defined breakpoints.

  • Add the responsive media queries at the very bottom of style.css by pasting:
@media (max-width: 768px) {
    :root {
        --font-size-xl: 2rem;
        --font-size-lg: 1.1rem;
    }

    .nav-links {
        gap: var(--spacing-md);
    }

    .project-grid {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 480px) {
    :root {
        --font-size-xl: 1.5rem;
    }

    .navbar {
        flex-direction: column;
        gap: var(--spacing-sm);
        padding: var(--spacing-sm) var(--spacing-md);
    }

    .nav-links {
        gap: var(--spacing-sm);
    }

    .contact-links {
        flex-direction: column;
        align-items: center;
    }
}

What do these breakpoints do?

At 768px (tablet), the project grid switches to a single column and font sizes shrink slightly. This prevents cards from being squished into an unreadable width.

At 480px (phone), the navbar stacks vertically so the logo and links do not overlap. The contact links also stack into a column for easier tapping.

  • Save style.css and refresh your browser.
  • Resize your browser window to a narrow width (under 480px). The navbar should stack vertically with the logo above the links.

Navbar not collapsing at narrow widths?

Check that the media query uses max-width: 480px (not min-width). Also make sure there are no typos in the property name flex-direction: column inside the 480px block.

My media queries don't seem to be taking effect. Help me debug.

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css and move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete style.css file. Compare it against yours to spot any differences.

:root {
    --color-bg: #f8f9fa;
    --color-text: #1a1a2e;
    --color-heading: #16213e;
    --color-accent: #0f3460;
    --color-card-bg: #ffffff;
    --color-card-shadow: rgba(0, 0, 0, 0.1);
    --color-nav-bg: #ffffff;
    --color-tag-bg: #e8edf3;
    --font-size-base: 1rem;
    --font-size-lg: 1.25rem;
    --font-size-xl: 2.5rem;
    --spacing-sm: 0.5rem;
    --spacing-md: 1rem;
    --spacing-lg: 2rem;
    --spacing-xl: 4rem;
    --border-radius: 8px;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html {
    scroll-behavior: smooth;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background-color: var(--color-bg);
    color: var(--color-text);
    line-height: 1.6;
}

.navbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--spacing-md) var(--spacing-lg);
    background-color: var(--color-nav-bg);
    box-shadow: 0 2px 4px var(--color-card-shadow);
    z-index: 1000;
}

.logo {
    font-size: var(--font-size-lg);
    font-weight: 700;
    color: var(--color-accent);
    text-decoration: none;
}

.nav-links {
    display: flex;
    list-style: none;
    gap: var(--spacing-lg);
}

.nav-links a {
    text-decoration: none;
    color: var(--color-text);
    font-weight: 500;
    transition: color 0.3s ease;
}

.nav-links a:hover {
    color: var(--color-accent);
}

.hero {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    min-height: 100vh;
    padding: var(--spacing-xl) var(--spacing-lg);
}

.hero h1 {
    font-size: var(--font-size-xl);
    color: var(--color-heading);
    margin-bottom: var(--spacing-md);
}

.tagline {
    font-size: var(--font-size-lg);
    color: var(--color-accent);
    opacity: 0.8;
}

.projects {
    padding: var(--spacing-xl) var(--spacing-lg);
    max-width: 1200px;
    margin: 0 auto;
}

.projects h2,
.about h2,
footer h2 {
    font-size: 2rem;
    color: var(--color-heading);
    margin-bottom: var(--spacing-lg);
    text-align: center;
}

.filter-bar {
    display: flex;
    justify-content: center;
    gap: var(--spacing-sm);
    margin-bottom: var(--spacing-lg);
    flex-wrap: wrap;
}

.filter-btn {
    padding: var(--spacing-sm) var(--spacing-md);
    border: 2px solid var(--color-accent);
    background: transparent;
    color: var(--color-accent);
    border-radius: 20px;
    cursor: pointer;
    font-weight: 500;
    transition: all 0.3s ease;
}

.filter-btn.active,
.filter-btn:hover {
    background-color: var(--color-accent);
    color: #ffffff;
}

.project-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: var(--spacing-lg);
}

.project-card {
    background-color: var(--color-card-bg);
    border-radius: var(--border-radius);
    box-shadow: 0 4px 6px var(--color-card-shadow);
    overflow: hidden;
    transition: transform 0.3s ease, box-shadow 0.3s ease, opacity 0.4s ease;
}

.project-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 16px var(--color-card-shadow);
}

.project-card.card-hidden {
    display: none;
}

.card-image img {
    width: 100%;
    height: 200px;
    object-fit: cover;
}

.card-content {
    padding: var(--spacing-md);
}

.card-content h3 {
    color: var(--color-heading);
    margin-bottom: var(--spacing-sm);
}

.card-content p {
    margin-bottom: var(--spacing-md);
    color: var(--color-text);
    opacity: 0.85;
}

.tech-tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-sm);
    margin-bottom: var(--spacing-md);
}

.tech-tags span {
    background-color: var(--color-tag-bg);
    padding: 0.25rem 0.75rem;
    border-radius: 12px;
    font-size: 0.85rem;
    color: var(--color-accent);
}

.card-link {
    display: inline-block;
    color: var(--color-accent);
    font-weight: 600;
    text-decoration: none;
}

.card-link:hover {
    text-decoration: underline;
}

.about {
    padding: var(--spacing-xl) var(--spacing-lg);
    max-width: 800px;
    margin: 0 auto;
    text-align: center;
}

.about p {
    font-size: var(--font-size-lg);
    line-height: 1.8;
}

footer {
    padding: var(--spacing-xl) var(--spacing-lg);
    text-align: center;
    background-color: var(--color-heading);
    color: #ffffff;
}

footer h2 {
    color: #ffffff;
}

.contact-links {
    display: flex;
    justify-content: center;
    gap: var(--spacing-lg);
    margin-bottom: var(--spacing-lg);
}

.contact-links a {
    color: #ffffff;
    text-decoration: none;
    font-weight: 500;
    padding: var(--spacing-sm) var(--spacing-md);
    border: 1px solid #ffffff;
    border-radius: 4px;
    transition: background-color 0.3s ease;
}

.contact-links a:hover {
    background-color: rgba(255, 255, 255, 0.1);
}

.copyright {
    opacity: 0.7;
    font-size: 0.9rem;
}

.hidden {
    opacity: 0;
    transform: translateY(20px);
    transition: opacity 0.6s ease, transform 0.6s ease;
}

.visible {
    opacity: 1;
    transform: translateY(0);
}

@media (max-width: 768px) {
    :root {
        --font-size-xl: 2rem;
        --font-size-lg: 1.1rem;
    }

    .nav-links {
        gap: var(--spacing-md);
    }

    .project-grid {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 480px) {
    :root {
        --font-size-xl: 1.5rem;
    }

    .navbar {
        flex-direction: column;
        gap: var(--spacing-sm);
        padding: var(--spacing-sm) var(--spacing-md);
    }

    .nav-links {
        gap: var(--spacing-sm);
    }

    .contact-links {
        flex-direction: column;
        align-items: center;
    }
}

Your portfolio now has a polished, professional layout that adapts to any screen size. Next up, you will bring it to life with JavaScript interactivity for filtering and scroll animations.

Add JavaScript Interactivity

Your portfolio looks great, but right now it's static. Visitors can't filter your projects by category, and the page loads everything at once without any visual flair.

In this step, you'll bring the site to life with JavaScript. You'll wire up the filter buttons so visitors can browse by category, and add scroll-triggered animations so content fades in as the viewer scrolls down the page.

In this step, get ready to:
  • Implement project filtering with click event listeners.
  • Add scroll-triggered fade-in animations using IntersectionObserver.
Implement project filtering

Your HTML already has filter buttons with data-filter attributes and project cards with data-category attributes. The JavaScript needs to connect the two so clicking a button shows only the matching cards.

  • In VS Code, open script.js and add the filtering logic by pasting the following code:
document.addEventListener("DOMContentLoaded", function () {
    // Project filtering
    const filterButtons = document.querySelectorAll(".filter-btn");
    const projectCards = document.querySelectorAll(".project-card");

    filterButtons.forEach(function (button) {
        button.addEventListener("click", function () {
            // Read which category this button filters for
            const filter = this.getAttribute("data-filter");

            // Remove active class from all buttons, then add to clicked one
            filterButtons.forEach(function (btn) {
                btn.classList.remove("active");
            });
            this.classList.add("active");

            // Show matching cards, hide non-matching ones
            projectCards.forEach(function (card) {
                if (filter === "all" || card.getAttribute("data-category") === filter) {
                    card.classList.remove("card-hidden");
                } else {
                    card.classList.add("card-hidden");
                }
            });
        });
    });
});

What does this code do?

Everything runs inside a DOMContentLoaded listener, which waits until the HTML is fully loaded before running.

  • filterButtons grabs every button with class .filter-btn from your HTML.
  • When a button is clicked, data-filter tells us which category was chosen (e.g. "cloud", "web", or "all").
  • The card-hidden class triggers display: none in your CSS, instantly hiding non-matching cards.
  • Save script.js.
  • Refresh your browser. Click the Cloud filter button. You should see only the Cloud Infrastructure card remain visible, and the other two cards disappear.
  • Click All to bring all cards back.

Clicking buttons does nothing?

  • Make sure you saved script.js and did a hard refresh (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to clear your browser cache.
  • Check that your index.html has <script src="script.js"></script> just before the closing </body> tag.
  • Open your browser's Developer Tools console (F12) and look for red error messages. A missing quote or bracket will show here.

Help me debug why my filter buttons aren't working

Add scroll-triggered animations

Your CSS already defines a .hidden class (invisible, shifted down) and a .visible class (fully visible, original position). The IntersectionObserver API watches elements as you scroll and fires a callback when they enter the viewport.

  • In script.js, find the closing }); at the very end of the file. Place your cursor on the blank line just above it (after the filtering logic's closing }); for the forEach) and paste the following code:
    // Scroll-triggered animations with IntersectionObserver
    const observerOptions = {
        threshold: 0.1,
        rootMargin: "0px 0px -50px 0px"
    };

    const observer = new IntersectionObserver(function (entries) {
        entries.forEach(function (entry) {
            if (entry.isIntersecting) {
                // Reveal the element with a fade-in/slide-up
                entry.target.classList.add("visible");
                entry.target.classList.remove("hidden");
            }
        });
    }, observerOptions);

    // Watch all project cards and the about section
    const animatedElements = document.querySelectorAll(".project-card, .about");
    animatedElements.forEach(function (element) {
        observer.observe(element);
    });

What does this code do?

  • observerOptions configures when the callback fires. threshold: 0.1 means the element only needs to be 10% visible. rootMargin: "0px 0px -50px 0px" shrinks the detection zone by 50px from the bottom, so animations trigger slightly before the element fully enters view.
  • When an element enters the viewport, the callback swaps .hidden for .visible. Your CSS transitions handle the smooth fade-in and slide-up from there.
  • animatedElements targets every .project-card and the .about section, which all start with the hidden class in your HTML.
  • Save script.js and refresh your browser.
  • Scroll down slowly from the hero section. You should see the project cards and the About section fade in and slide up as they enter your viewport.

Cards don't animate on scroll?

  • Check that your project cards in index.html have the class hidden (e.g. class="project-card hidden"). Without this starting class, there's nothing for the observer to swap.
  • Make sure the IntersectionObserver code is inside the DOMContentLoaded wrapper, not outside of it. It should sit after the filtering code but before the final });.
  • If cards appear already visible without animation, scroll to the top of the page and refresh. The cards need to start off-screen to animate in.

Help me debug why my scroll animations aren't triggering

Why IntersectionObserver instead of scroll events?

A traditional scroll event listener fires dozens of times per second as the user scrolls, which can cause lag. IntersectionObserver is handled by the browser itself, only running your code when elements actually cross the visibility threshold. It's more performant and much simpler to configure.

✔️ Awesome, I've got everything!

Great. Double check you've saved script.js and both the filtering and scroll animations work in your browser.

ⓧ I'd like to double check the full code

Here's the complete script.js file. Compare yours against it:

document.addEventListener("DOMContentLoaded", function () {
    // Project filtering
    const filterButtons = document.querySelectorAll(".filter-btn");
    const projectCards = document.querySelectorAll(".project-card");

    filterButtons.forEach(function (button) {
        button.addEventListener("click", function () {
            const filter = this.getAttribute("data-filter");

            filterButtons.forEach(function (btn) {
                btn.classList.remove("active");
            });
            this.classList.add("active");

            projectCards.forEach(function (card) {
                if (filter === "all" || card.getAttribute("data-category") === filter) {
                    card.classList.remove("card-hidden");
                } else {
                    card.classList.add("card-hidden");
                }
            });
        });
    });

    // Scroll-triggered animations with IntersectionObserver
    const observerOptions = {
        threshold: 0.1,
        rootMargin: "0px 0px -50px 0px"
    };

    const observer = new IntersectionObserver(function (entries) {
        entries.forEach(function (entry) {
            if (entry.isIntersecting) {
                entry.target.classList.add("visible");
                entry.target.classList.remove("hidden");
            }
        });
    }, observerOptions);

    const animatedElements = document.querySelectorAll(".project-card, .about");
    animatedElements.forEach(function (element) {
        observer.observe(element);
    });
});

Your portfolio is now fully interactive. Visitors can filter projects by category and enjoy smooth scroll animations as they explore. Next up, you'll deploy the whole thing live to GitHub Pages so anyone with the URL can see your work.

Deploy to GitHub Pages

Your portfolio website is fully built and working locally. Now it is time to put it on the internet where anyone can see it.

In this step, you will push your code to GitHub and enable GitHub Pages to host your site for free. Once enabled, every future update is a single push away from going live.

In this step, get ready to:
  • Push your portfolio code to GitHub.
  • Enable GitHub Pages to deploy your site.
  • Verify your live site is publicly accessible.
Push your code to GitHub

Git tracks changes in three stages: staging (selecting files), committing (saving a snapshot), and pushing (uploading to the remote). You will do all three now to get your files onto GitHub.

  • Stage, commit, and push all your files by running these commands in the VS Code terminal:
git add .
git commit -m "Initial portfolio site"
git push origin main

What do these commands do?

  • git add . stages every file in the current folder, telling Git you want to include them in the next snapshot.
  • git commit -m "Initial portfolio site" saves a snapshot of the staged files with a descriptive message.
  • git push origin main uploads your commit to the main branch of your GitHub repository.
  • You should see output confirming the push succeeded, including a line like To https://github.com/... followed by a branch reference.

Push rejected or authentication error?

If Git asks for your username and password, GitHub no longer accepts passwords for HTTPS pushes. You need a personal access token or SSH key. Visit your GitHub account settings to create one.

If you see a "rejected" error, it likely means the remote has a commit (like the README) that your local branch does not have. Pull first with git pull origin main --rebase then push again.

Help me fix my git push error

Enable GitHub Pages in your repository settings

Your code is on GitHub, but it is not being served as a website yet. You need to tell GitHub Pages which branch and folder to deploy from.

  • Open your repository on GitHub in your browser.
  • Click Settings in the top navigation bar of your repository.
  • In the "Code and automation" section of the left sidebar, click Pages.
  • Under "Build and deployment", find the Source dropdown and select Deploy from a branch.
  • In the Branch section below that, select main as the branch and / (root) as the folder.
  • Click Save.

What is GitHub Pages doing?

GitHub Pages takes the files in your selected branch and folder and serves them as a static website. Since your index.html is in the root of the main branch, it becomes the homepage at your URL.

Changes can take up to 10 minutes to publish after you push, though it is often much faster.

Verify your live site

After a few minutes, GitHub finishes building your site and it becomes publicly accessible at your personal URL.

  • Open a new browser tab and visit your site at:
https://[[GITHUB_USERNAME="yourusername"]].github.io
  • Verify your hero section, project cards, about section, and footer all appear correctly.
  • Click the filter buttons to confirm category filtering works on the live site.
  • Scroll down the page and confirm the fade-in animations trigger as sections come into view.
  • Click the navigation links (Projects, About, Contact) to confirm smooth scrolling works.

Seeing a 404 page?

If you see a 404 error, wait a few more minutes. GitHub Pages can take up to 10 minutes to finish its first build.

Also check that your repository name matches the pattern yourusername.github.io exactly (using your actual GitHub username). If the name is different, GitHub Pages will not serve it at the expected URL.

Help me troubleshoot my GitHub Pages 404

Why GitHub Pages for deployment?

GitHub Pages is free, requires no credit card, and integrates directly with Git. Every time you push to your main branch, your site updates automatically.

Alternatives like Netlify or Vercel add complexity that is unnecessary for a static portfolio. GitHub Pages keeps everything in one ecosystem you already use.

Secret mission

Add a Dark/Light Theme Toggle

Your portfolio looks great, but what about visitors who prefer browsing in the dark? In this secret mission, you'll add a theme toggle that switches between light and dark modes. The toggle persists across page refreshes using localStorage.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and deploys to GitHub Pages for free, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Local project folder (yourusername.github.io on your Desktop).
  • GitHub repository (yourusername.github.io with GitHub Pages deployment).

Keep everything running

No action needed. Choose this if you plan to keep updating your portfolio with new projects and content.

  • Your site stays live at https://yourusername.github.io for free.
  • Any future changes you push to the main branch automatically deploy within minutes.
  • Git remains installed on your machine for other projects.

Pause - I'll come back to this later

There is nothing to pause for this project. GitHub Pages is free and has no running processes that consume resources on your machine.

  • Your site continues to be served for free with no action required.
  • Your local files stay on disk until you choose to delete them.
  • When you are ready to return, open the yourusername.github.io folder in VS Code and pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. This takes down your live site and removes all local files.

  • Delete your local project folder by running one of these commands in your terminal:
  • macOS/Linux:
  • Windows (PowerShell):
cd ~/Desktop
rm -rf yourusername.github.io
cd ~\Desktop
Remove-Item -Recurse -Force yourusername.github.io
  • Delete the GitHub repository (this also removes the GitHub Pages site):
  • Go to your repository on GitHub.
  • Click Settings in the top menu.
  • Scroll down to the Danger Zone section.
  • Click Delete this repository.
  • Click I want to delete this repository.
  • Read the warnings and click I have read and understand these effects.
  • Type your repository name to confirm, then click Delete this repository.

That's a Wrap!

That's a Wrap!

Nice work! You just built and deployed a professional portfolio website from scratch using vanilla HTML, CSS, and JavaScript.

You've learned how to:

  • Build a complete responsive website with semantic HTML, CSS Grid, custom properties, and media queries.
  • Add JavaScript interactivity including category-based project filtering and scroll-triggered animations with IntersectionObserver.
  • Deploy a live site to GitHub Pages connected to a Git workflow so future updates are a single push away.
  • Secret Mission: implemented a dark/light theme toggle with persistent user preference via localStorage.

Ready to quiz yourself?