Build and Deploy a Portfolio Website

Build a responsive portfolio site with HTML, CSS, and JS and deploy it live.

Introduction

30 Second Summary

Everyone needs an online presence. Whether you're applying for your first tech job or sharing side projects with friends, a personal website gives you a place to point people to.

In this project, you will build a responsive portfolio website from scratch using HTML, CSS, and JavaScript, then deploy it live to the internet with GitHub Pages.

What You'll Build

A single-page portfolio site with a hero section, project showcase grid, dark/light theme toggle, and smooth scroll navigation.

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

  • A complete index.html with semantic HTML5 structure including navigation, hero, projects, and contact sections.
  • A style.css using CSS custom properties for theming, Flexbox, Grid, and responsive design.
  • A script.js with a dark/light theme toggle that remembers your preference and smooth scroll navigation.
  • A live site deployed to GitHub Pages at your own URL that you can share with anyone.
  • Secret Mission: Add scroll-triggered fade-in animations to your project cards using the Intersection Observer API.

Are there any prerequisites?

None. This project includes full setup instructions for every tool you need. You do not need any prior coding experience to get started.

Set Up Your Development Environment

Every website starts with the right tools. Before you write a single line of HTML, you need a code editor to write in, a live preview to see your changes instantly, and version control to track your progress.

In this step, you will install VS Code as your code editor, add the Live Server extension for instant browser previews, set up Git for version control, and create the project folder where all your portfolio files will live.

In this step, get ready to:
  • Install VS Code and the Live Server extension.
  • Install Git and create a GitHub account.
  • Create your project folder and initialize a Git repository.
Install VS Code and the Live Server extension

VS Code is a free code editor built for web development. The Live Server extension lets you see your website update in real time as you save files.

  • Download VS Code from code.visualstudio.com.
  • Run the installer and follow the prompts to complete installation.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.

What is VS Code?

VS Code (Visual Studio Code) is a lightweight, free code editor made by Microsoft. It supports extensions that add extra functionality, like the Live Server extension you are about to install.

  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type Live Server in the search bar at the top of the panel.
  • Find the extension by Ritwick Dey and click Install.

Look at the bottom-right corner of the VS Code window. You should see a Go Live button in the status bar.

Don't see the Go Live button?

Make sure you installed the extension named Live Server by Ritwick Dey. There are similarly named extensions from other publishers.

Try closing and reopening VS Code after installing the extension. The Go Live button only appears once the extension is fully loaded.

Why isn't my Live Server Go Live button showing up?

Install Git and create a GitHub account

Git tracks every change you make to your code, so you can always go back to a previous version. GitHub is a website that hosts your Git repositories online and will serve your finished portfolio as a live website.

  • Download and install Git from git-scm.com.
  • Run the installer and accept the default options at each prompt.

After installation, verify Git is working. Open the VS Code terminal by pressing Ctrl+` (the Control key plus the backtick key, located below Escape).

  • Confirm Git is installed by running this command:
git --version

✔️ I see a version number

You should see something like git version 2.x.x. Git is ready to go.

ⓧ Command not found

This means Git did not install correctly or the terminal cannot find it.

  • Close and reopen VS Code completely (not just the terminal). The terminal needs to reload its system paths.
  • Run git --version again.
  • If it still fails, re-download the installer from git-scm.com and run it again. On Windows, make sure Git from the command line and also from 3rd-party software is selected during installation.

Help me fix Git not being recognized in my VS Code terminal.

  • Create a free GitHub account at github.com if you do not have one already.
  • Remember your GitHub username. You will need it in a later step to deploy your site.
Create your project folder and initialize Git

Now you will create the folder that holds all your portfolio files and set it up as a Git repository.

  • In the VS Code terminal, move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder by running:
mkdir portfolio
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the portfolio folder, and click Open.

Trust the authors?

VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue. This prompt appears whenever you open a new folder for the first time.

  • Open a fresh terminal by pressing Ctrl+`. The terminal should now show portfolio as your current folder.
  • Initialize a Git repository by running:
git init

What does git init do?

This command creates a hidden .git folder inside your project. Git uses this folder to track every change you make. You only run this once per project.

You should see a message that says Initialized empty Git repository followed by your folder path.

  • Create your three project files by running this command:

macOS

touch index.html style.css script.js

Windows

New-Item -ItemType "File" -Path "index.html", "style.css", "script.js"
  • Confirm the files exist by running:
ls

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

Don't see your files?

Make sure your terminal is inside the portfolio folder. Check the path shown in your terminal prompt. If it does not end with portfolio, run cd ~/Desktop/portfolio and try the file creation command again.

Help me create my project files in the right folder.

You should also see these three files appear in the VS Code Explorer sidebar on the left.

Your development environment is ready. Next up, you will build the HTML structure of your portfolio page.

Build the HTML Structure

Your development environment is ready with VS Code, Live Server, and Git all set up. Now it's time to build the actual page your visitors will see.

Every website starts with HTML (HyperText Markup Language), the language that defines the structure and content of a web page. In this step, you'll write the HTML that gives your portfolio its skeleton: a navigation bar, a hero introduction, project cards, and a contact section.

In this step, get ready to:
  • Set up the HTML boilerplate and navigation bar.
  • Build the hero, about, and projects sections.
  • Add the contact section, footer, and preview the page live.
Set up the HTML boilerplate and navigation

The boilerplate is the standard code every HTML page needs before you add any visible content. It tells the browser what language the page is in, how to handle different screen sizes, and which external resources to load.

  • In VS Code, click on index.html in the Explorer sidebar to open the file.
  • Copy the following code into index.html:
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Your Name - Portfolio</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
</head>
<body>

</body>
</html>

What does this code do?

This sets up the invisible foundation of your page.

  • <!DOCTYPE html> tells the browser to use modern HTML5 standards.
  • data-theme="light" on the <html> tag sets the initial color theme. You'll use this later for the dark/light toggle.
  • The viewport meta tag ensures the page scales correctly on mobile devices.
  • The Google Fonts preconnect links speed up font loading by establishing early connections. The third link loads the Inter font.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • You won't see a visible change yet because the page has no content inside the <body> tags. The next chunk adds the navigation.

The navigation bar gives visitors a way to jump between sections of your portfolio. It sits inside a <header> element at the top of the page body.

  • In index.html, find the empty line between <body> and </body>.
  • Copy the following code between those tags:
    <header>
        <nav class="navbar">
            <a href="#" class="logo">Your Name</a>
            <div class="nav-links">
                <a href="#about">About</a>
                <a href="#projects">Projects</a>
                <a href="#contact">Contact</a>
                <button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme">🌙</button>
            </div>
        </nav>
    </header>

What does this code do?

This creates the navigation bar that will stick to the top of your page once styled.

  • The .logo link displays your name as the site branding.
  • Each <a href="#about"> link points to a section ID on the page, so clicking it will scroll the visitor there.
  • The #theme-toggle button will eventually switch between dark and light themes. The moon emoji is its initial icon.
  • Save index.html.
  • Start Live Server by clicking Go Live in the bottom-right status bar of VS Code.
  • Your browser opens automatically. You should see your name, the words About, Projects, Contact, and a moon emoji displayed as plain unstyled text.

Why does it look unstyled?

HTML defines the structure and content of a page, but not how it looks. Your style.css file is still empty. You'll add all the visual styling in the next step.

Don't see the Go Live button?

Make sure you opened the portfolio folder (not just the file) in VS Code. Live Server needs a folder open in the workspace to show the button.

If you still don't see it, try right-clicking inside the index.html editor and selecting Open with Live Server from the context menu.

Help me find and start Live Server in VS Code.

Build the hero, about, and projects sections

The main content of your portfolio lives inside a <main> element. This is a semantic HTML tag that tells browsers and screen readers that this is the primary content of the page.

  • In index.html, place your cursor on the blank line after the closing </header> tag.
  • Copy the following code below it:
    <main>
        <section class="hero" id="hero">
            <h1>Hi, I'm <span class="highlight">Your Name</span></h1>
            <p class="subtitle">I build things for the web.</p>
            <a href="#projects" class="cta-button">View My Work</a>
        </section>

        <section class="about" id="about">
            <h2>About Me</h2>
            <p>I'm a web developer passionate about creating clean, responsive, and user-friendly websites. I enjoy turning ideas into reality using HTML, CSS, and JavaScript.</p>
        </section>

What does this code do?

These two sections create the top half of your portfolio content.

  • The .hero section is the first thing visitors see. It contains your name, a tagline, and a call-to-action button linking to your projects.
  • The .highlight span wraps your name so you can style it with an accent color later.
  • The .about section gives visitors a brief introduction to who you are.
  • Save index.html and check your browser.
  • You should see your heading "Hi, I'm Your Name", the subtitle, a "View My Work" link, an "About Me" heading, and the bio paragraph.

Page not updating automatically?

Live Server reloads the browser when you save. If nothing changed, make sure you actually saved the file. Look for a dot on the file tab in VS Code, which indicates unsaved changes.

My Live Server is not auto-reloading when I save.

Now add the projects section directly below the about section. This uses a grid of cards to showcase your work.

  • In index.html, place your cursor on the line after the closing </section> tag of the about section.
  • Copy the following code below it:
        <section class="projects" id="projects">
            <h2>Projects</h2>
            <div class="project-grid">
                <div class="project-card">
                    <h3>Project One</h3>
                    <p>A brief description of your first project and what technologies you used to build it.</p>
                    <a href="#" class="project-link">View Project &rarr;</a>
                </div>
                <div class="project-card">
                    <h3>Project Two</h3>
                    <p>A brief description of your second project and what technologies you used to build it.</p>
                    <a href="#" class="project-link">View Project &rarr;</a>
                </div>
                <div class="project-card">
                    <h3>Project Three</h3>
                    <p>A brief description of your third project and what technologies you used to build it.</p>
                    <a href="#" class="project-link">View Project &rarr;</a>
                </div>
            </div>
        </section>

What does this code do?

This creates a grid of three project cards, each with a title, description, and link.

  • The .project-grid div will become a CSS Grid layout that automatically arranges cards into columns.
  • Each .project-card is a reusable structure you can duplicate when you have more projects to add.
  • &rarr; is an HTML entity that renders as a right arrow character.
  • Save index.html and check your browser.
  • You should now see three project titles (Project One, Project Two, Project Three) with descriptions and "View Project" links below your about section.

Seeing a blank page or broken layout?

Check that every opening tag has a matching closing tag. A missing </div> or </section> can break the page layout entirely.

Help me find a missing closing tag in my HTML.

Add the contact section and footer

The final pieces of your page are a contact section and a footer with social links. The contact section belongs inside <main>, while the footer sits after it.

  • In index.html, place your cursor on the line after the closing </section> tag of the projects section.
  • Copy the following code below it to close out the main content area:
        <section class="contact" id="contact">
            <h2>Get In Touch</h2>
            <p>I'm always open to new opportunities and collaborations. Feel free to reach out!</p>
            <a href="mailto:your-email@example.com" class="cta-button">Say Hello</a>
        </section>
    </main>

What does mailto: do?

The mailto: link opens the visitor's default email app with your address pre-filled. You can replace your-email@example.com with your real email address later.

The closing </main> tag marks the end of all primary page content. Everything after this (footer, script) is supplementary.

  • Save index.html and check your browser.
  • You should see "Get In Touch", a brief message, and a "Say Hello" link below your project cards.

Contact section not appearing?

Make sure the contact section code is placed after the projects </section> closing tag but before the </main> tag. If you accidentally placed it after </main>, it will still render but won't be semantically correct.

Help me check the position of my contact section.

Now add the footer and JavaScript link after the closing </main> tag. These are the last elements before </body>.

  • In index.html, place your cursor on the blank line after the closing </main> tag.
  • Copy the following code below it:
    <footer>
        <div class="footer-content">
            <p>&copy; 2026 Your Name. All rights reserved.</p>
            <div class="social-links">
                <a href="https://github.com" target="_blank" rel="noopener noreferrer">GitHub</a>
                <a href="https://linkedin.com" target="_blank" rel="noopener noreferrer">LinkedIn</a>
            </div>
        </div>
    </footer>

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

What does this code do?

The footer and script tag complete your page structure.

  • &copy; renders as the copyright symbol (©).
  • target="_blank" opens links in a new browser tab so visitors don't leave your portfolio.
  • rel="noopener noreferrer" is a security best practice that prevents the new page from accessing your page's data.
  • The <script> tag at the bottom loads your JavaScript file. Placing it here ensures all HTML loads before the script runs.
  • Save index.html and check your browser.
  • You should see copyright text and GitHub/LinkedIn links at the very bottom of the page.

Footer not showing at the bottom?

Make sure the footer code is placed after the </main> closing tag, not inside it. Also confirm that the <script> tag comes after the footer but before </body>.

Help me check my footer placement in index.html.

✔️ Awesome, I've got everything!

Great. Double check you've saved index.html and your browser shows all sections from the navigation down to the footer.

ⓧ I'd like to double check the full code

Here is the complete index.html file at this point. Compare it with yours:

<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Your Name - Portfolio</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <nav class="navbar">
            <a href="#" class="logo">Your Name</a>
            <div class="nav-links">
                <a href="#about">About</a>
                <a href="#projects">Projects</a>
                <a href="#contact">Contact</a>
                <button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme">🌙</button>
            </div>
        </nav>
    </header>

    <main>
        <section class="hero" id="hero">
            <h1>Hi, I'm <span class="highlight">Your Name</span></h1>
            <p class="subtitle">I build things for the web.</p>
            <a href="#projects" class="cta-button">View My Work</a>
        </section>

        <section class="about" id="about">
            <h2>About Me</h2>
            <p>I'm a web developer passionate about creating clean, responsive, and user-friendly websites. I enjoy turning ideas into reality using HTML, CSS, and JavaScript.</p>
        </section>

        <section class="projects" id="projects">
            <h2>Projects</h2>
            <div class="project-grid">
                <div class="project-card">
                    <h3>Project One</h3>
                    <p>A brief description of your first project and what technologies you used to build it.</p>
                    <a href="#" class="project-link">View Project &rarr;</a>
                </div>
                <div class="project-card">
                    <h3>Project Two</h3>
                    <p>A brief description of your second project and what technologies you used to build it.</p>
                    <a href="#" class="project-link">View Project &rarr;</a>
                </div>
                <div class="project-card">
                    <h3>Project Three</h3>
                    <p>A brief description of your third project and what technologies you used to build it.</p>
                    <a href="#" class="project-link">View Project &rarr;</a>
                </div>
            </div>
        </section>

        <section class="contact" id="contact">
            <h2>Get In Touch</h2>
            <p>I'm always open to new opportunities and collaborations. Feel free to reach out!</p>
            <a href="mailto:your-email@example.com" class="cta-button">Say Hello</a>
        </section>
    </main>

    <footer>
        <div class="footer-content">
            <p>&copy; 2026 Your Name. All rights reserved.</p>
            <div class="social-links">
                <a href="https://github.com" target="_blank" rel="noopener noreferrer">GitHub</a>
                <a href="https://linkedin.com" target="_blank" rel="noopener noreferrer">LinkedIn</a>
            </div>
        </div>
    </footer>

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

Your HTML structure is complete. Every section of your portfolio is in place, from the navigation bar down to the footer. Next up, you'll bring this page to life with CSS styling that adds colors, layout, and visual polish.

Style Your Portfolio with CSS

Your HTML structure is complete and you can see all the content in your browser. Right now it looks plain because there are no styles applied.

In this step, you will transform that raw HTML into a polished, modern-looking portfolio using CSS. You will define a color system with CSS Custom Properties (variables), build layouts with Flexbox and CSS Grid, and make everything look great on any screen size.

In this step, get ready to:
  • Define CSS custom properties for light and dark themes.
  • Style the navigation bar and hero section with Flexbox and fluid typography.
  • Build a responsive projects grid and add a mobile-friendly layout.
Define CSS custom properties and base styles

Before writing any visual styles, you need to define a consistent set of colors and fonts that the entire page will use. CSS custom properties let you define values once and reference them everywhere.

  • In VS Code, click on style.css in the Explorer sidebar to open it.
  • Add the following code to define your light theme variables and dark theme overrides:
:root {
    --bg-primary: #ffffff;
    --bg-secondary: #f8f9fa;
    --text-primary: #1a1a2e;
    --text-secondary: #4a4a6a;
    --accent: #2563eb;
    --accent-hover: #1d4ed8;
    --card-bg: #ffffff;
    --card-shadow: rgba(0, 0, 0, 0.08);
    --card-shadow-hover: rgba(0, 0, 0, 0.15);
    --nav-bg: rgba(255, 255, 255, 0.9);
    --border: #e2e8f0;
    --font-family: 'Inter', sans-serif;
}

[data-theme="dark"] {
    --bg-primary: #0f172a;
    --bg-secondary: #1e293b;
    --text-primary: #f1f5f9;
    --text-secondary: #94a3b8;
    --accent: #3b82f6;
    --accent-hover: #60a5fa;
    --card-bg: #1e293b;
    --card-shadow: rgba(0, 0, 0, 0.3);
    --card-shadow-hover: rgba(0, 0, 0, 0.5);
    --nav-bg: rgba(15, 23, 42, 0.9);
    --border: #334155;
}

What does this code do?

The :root block defines CSS variables (like --bg-primary and --accent) that hold your light theme colors. Every element on the page can reference these instead of hardcoding hex values.

The [data-theme="dark"] block overrides those same variables with darker colors. When the data-theme attribute on your <html> tag changes to dark, every element instantly picks up the new values.

Why CSS variables instead of hardcoded colors?

Without variables, switching between dark and light themes would mean writing hundreds of duplicate style rules. With variables, you define colors once and the theme toggle only needs to swap the variable values. This is how professional design systems work.

  • Now add the base reset and body styles directly below the dark theme block:
*,
*::before,
*::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: var(--font-family);
    background-color: var(--bg-primary);
    color: var(--text-primary);
    line-height: 1.6;
    transition: background-color 0.3s ease, color 0.3s ease;
}

What does this code do?

The * selector resets all elements to box-sizing: border-box and removes default margins and padding. This gives you a clean canvas where sizes behave predictably.

The body styles apply your font, background color, and text color using the variables you just defined. The transition property means when the theme switches, colors fade smoothly instead of snapping.

  • Save style.css.
  • Check your browser (Live Server should auto-refresh). You should see the page now uses the Inter font with a clean white background, and all default spacing between elements is gone.

Page looks the same as before?

Make sure your style.css file is saved. Check that the <link rel="stylesheet" href="style.css"> tag in index.html matches the filename exactly (case-sensitive).

Why isn't my CSS being applied to the page?

Style the navigation and hero section

With your color system in place, you can now style the two most prominent parts of the page: the sticky navigation bar and the large hero area that visitors see first.

  • Add the navigation styles below the body block in style.css:
/* Navigation */
.navbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 2rem;
    position: sticky;
    top: 0;
    background-color: var(--nav-bg);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--border);
    z-index: 100;
}

.logo {
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--text-primary);
    text-decoration: none;
}

.nav-links {
    display: flex;
    align-items: center;
    gap: 2rem;
}

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

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

.theme-toggle {
    background: none;
    border: 2px solid var(--border);
    border-radius: 8px;
    padding: 0.5rem;
    cursor: pointer;
    font-size: 1.25rem;
    line-height: 1;
    transition: border-color 0.2s ease;
}

.theme-toggle:hover {
    border-color: var(--accent);
}

What does this code do?

The .navbar uses Flexbox (display: flex) to push the logo to the left and links to the right with justify-content: space-between. The position: sticky keeps it pinned to the top as you scroll.

The backdrop-filter: blur(10px) creates a frosted-glass effect so content behind the nav is softly blurred. The transition on links and the toggle button makes color changes animate smoothly on hover.

  • Save style.css and check your browser.
  • You should see a sleek navigation bar stuck to the top with your name on the left and links on the right. Hover over a link to see it turn blue.

Navigation not sticking to the top?

Make sure position: sticky and top: 0 are both present in your .navbar block. If you're missing top: 0, the browser doesn't know where to stick it.

Why isn't my sticky navbar working?

  • Now add the hero section styles below the navigation styles:
/* Hero Section */
.hero {
    min-height: 80vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    padding: 2rem;
}

.hero h1 {
    font-size: clamp(2rem, 5vw, 3.5rem);
    font-weight: 700;
    margin-bottom: 1rem;
}

.highlight {
    color: var(--accent);
}

.subtitle {
    font-size: clamp(1.1rem, 2.5vw, 1.5rem);
    color: var(--text-secondary);
    margin-bottom: 2rem;
}

.cta-button {
    display: inline-block;
    padding: 0.75rem 1.5rem;
    background-color: var(--accent);
    color: #ffffff;
    text-decoration: none;
    border-radius: 8px;
    font-weight: 600;
    transition: background-color 0.2s ease, transform 0.2s ease;
}

.cta-button:hover {
    background-color: var(--accent-hover);
    transform: translateY(-2px);
}

What does this code do?

The .hero section takes up at least 80% of the screen height (min-height: 80vh) and uses Flexbox to center everything vertically and horizontally.

The clamp() function creates fluid typography. For example, clamp(2rem, 5vw, 3.5rem) means the heading is never smaller than 2rem, never larger than 3.5rem, and scales with the viewport width in between. This makes the text responsive without needing a media query.

The .cta-button lifts slightly upward on hover (transform: translateY(-2px)) giving a satisfying interactive feel.

  • Save style.css and check your browser.
  • You should see your name displayed large and centered in the middle of the viewport with a blue "View My Work" button below it. Hover over the button to see it lift up slightly.

Hero text not centered on the page?

Confirm that .hero has all three centering properties: justify-content: center, align-items: center, and flex-direction: column. Missing any one of these will throw off the alignment.

Help me center my hero section content

Style the projects grid and responsive layout

The navigation and hero look great. Now you need to style the content sections below: the about area, the project cards grid, the contact section, and the footer.

  • Add the content section styles below the hero styles in style.css:
/* About Section */
.about {
    max-width: 700px;
    margin: 0 auto;
    padding: 4rem 2rem;
}

.about h2,
.projects h2,
.contact h2 {
    font-size: 2rem;
    margin-bottom: 1.5rem;
}

.about p {
    color: var(--text-secondary);
    font-size: 1.1rem;
}

/* Projects Section */
.projects {
    padding: 4rem 2rem;
    max-width: 1100px;
    margin: 0 auto;
}

.project-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1.5rem;
    margin-top: 1rem;
}

.project-card {
    background-color: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 2rem;
    box-shadow: 0 2px 8px var(--card-shadow);
    transition: box-shadow 0.3s ease, transform 0.3s ease;
}

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

.project-card h3 {
    margin-bottom: 0.75rem;
    font-size: 1.25rem;
}

.project-card p {
    color: var(--text-secondary);
    margin-bottom: 1rem;
    font-size: 0.95rem;
}

.project-link {
    color: var(--accent);
    text-decoration: none;
    font-weight: 600;
    transition: color 0.2s ease;
}

.project-link:hover {
    color: var(--accent-hover);
}

What does this code do?

The .project-grid uses CSS Grid with repeat(auto-fit, minmax(280px, 1fr)). This creates a responsive grid that automatically adjusts the number of columns based on available space. Each card is at least 280px wide, and the grid fills remaining space evenly.

The .project-card hover effect combines a larger shadow with a slight upward shift (translateY(-4px)). This makes cards feel interactive and "liftable" when the user hovers over them.

  • Save style.css and check your browser.
  • You should see three project cards arranged in a row. Hover over a card to see it lift up with a deeper shadow. Try resizing your browser window narrower to watch the cards automatically reflow into fewer columns.

Cards all stacked in one column?

Check that your .project-grid has display: grid and the grid-template-columns property. If you only have display: grid without columns defined, everything defaults to a single column.

Why aren't my CSS Grid cards showing side by side?

  • Now add the contact section, footer, and responsive styles to finish the stylesheet:
/* Contact Section */
.contact {
    text-align: center;
    padding: 4rem 2rem;
    max-width: 600px;
    margin: 0 auto;
}

.contact p {
    color: var(--text-secondary);
    margin-bottom: 2rem;
    font-size: 1.1rem;
}

/* Footer */
footer {
    border-top: 1px solid var(--border);
    padding: 2rem;
    margin-top: 4rem;
}

.footer-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    max-width: 1100px;
    margin: 0 auto;
}

.footer-content p {
    color: var(--text-secondary);
    font-size: 0.9rem;
}

.social-links {
    display: flex;
    gap: 1.5rem;
}

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

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

/* Responsive */
@media (max-width: 768px) {
    .navbar {
        padding: 1rem;
    }

    .nav-links {
        gap: 1rem;
    }

    .footer-content {
        flex-direction: column;
        gap: 1rem;
        text-align: center;
    }
}

What does this code do?

The footer uses Flexbox to place the copyright on the left and social links on the right, matching the navigation layout pattern.

The @media (max-width: 768px) query activates on screens smaller than 768px (tablets and phones). It reduces nav spacing, tightens link gaps, and stacks the footer content vertically so nothing overflows on small screens.

  • Save style.css and check your browser.
  • Scroll down to see the contact section centered with the "Say Hello" button, and the footer showing copyright on the left and social links on the right. Resize your browser to under 768px wide to see the footer stack vertically.

Footer links not appearing side by side?

Make sure .social-links has display: flex and gap: 1.5rem. Without display: flex, the links will stack vertically by default since <a> tags are inline elements inside a block container.

Help me fix my footer layout

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css and your browser shows the fully styled portfolio.

ⓧ I'd like to double check the full code

Here is the complete style.css file. Compare yours against it:

:root {
    --bg-primary: #ffffff;
    --bg-secondary: #f8f9fa;
    --text-primary: #1a1a2e;
    --text-secondary: #4a4a6a;
    --accent: #2563eb;
    --accent-hover: #1d4ed8;
    --card-bg: #ffffff;
    --card-shadow: rgba(0, 0, 0, 0.08);
    --card-shadow-hover: rgba(0, 0, 0, 0.15);
    --nav-bg: rgba(255, 255, 255, 0.9);
    --border: #e2e8f0;
    --font-family: 'Inter', sans-serif;
}

[data-theme="dark"] {
    --bg-primary: #0f172a;
    --bg-secondary: #1e293b;
    --text-primary: #f1f5f9;
    --text-secondary: #94a3b8;
    --accent: #3b82f6;
    --accent-hover: #60a5fa;
    --card-bg: #1e293b;
    --card-shadow: rgba(0, 0, 0, 0.3);
    --card-shadow-hover: rgba(0, 0, 0, 0.5);
    --nav-bg: rgba(15, 23, 42, 0.9);
    --border: #334155;
}

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

body {
    font-family: var(--font-family);
    background-color: var(--bg-primary);
    color: var(--text-primary);
    line-height: 1.6;
    transition: background-color 0.3s ease, color 0.3s ease;
}

/* Navigation */
.navbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 2rem;
    position: sticky;
    top: 0;
    background-color: var(--nav-bg);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--border);
    z-index: 100;
}

.logo {
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--text-primary);
    text-decoration: none;
}

.nav-links {
    display: flex;
    align-items: center;
    gap: 2rem;
}

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

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

.theme-toggle {
    background: none;
    border: 2px solid var(--border);
    border-radius: 8px;
    padding: 0.5rem;
    cursor: pointer;
    font-size: 1.25rem;
    line-height: 1;
    transition: border-color 0.2s ease;
}

.theme-toggle:hover {
    border-color: var(--accent);
}

/* Hero Section */
.hero {
    min-height: 80vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    padding: 2rem;
}

.hero h1 {
    font-size: clamp(2rem, 5vw, 3.5rem);
    font-weight: 700;
    margin-bottom: 1rem;
}

.highlight {
    color: var(--accent);
}

.subtitle {
    font-size: clamp(1.1rem, 2.5vw, 1.5rem);
    color: var(--text-secondary);
    margin-bottom: 2rem;
}

.cta-button {
    display: inline-block;
    padding: 0.75rem 1.5rem;
    background-color: var(--accent);
    color: #ffffff;
    text-decoration: none;
    border-radius: 8px;
    font-weight: 600;
    transition: background-color 0.2s ease, transform 0.2s ease;
}

.cta-button:hover {
    background-color: var(--accent-hover);
    transform: translateY(-2px);
}

/* About Section */
.about {
    max-width: 700px;
    margin: 0 auto;
    padding: 4rem 2rem;
}

.about h2,
.projects h2,
.contact h2 {
    font-size: 2rem;
    margin-bottom: 1.5rem;
}

.about p {
    color: var(--text-secondary);
    font-size: 1.1rem;
}

/* Projects Section */
.projects {
    padding: 4rem 2rem;
    max-width: 1100px;
    margin: 0 auto;
}

.project-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1.5rem;
    margin-top: 1rem;
}

.project-card {
    background-color: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 2rem;
    box-shadow: 0 2px 8px var(--card-shadow);
    transition: box-shadow 0.3s ease, transform 0.3s ease;
}

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

.project-card h3 {
    margin-bottom: 0.75rem;
    font-size: 1.25rem;
}

.project-card p {
    color: var(--text-secondary);
    margin-bottom: 1rem;
    font-size: 0.95rem;
}

.project-link {
    color: var(--accent);
    text-decoration: none;
    font-weight: 600;
    transition: color 0.2s ease;
}

.project-link:hover {
    color: var(--accent-hover);
}

/* Contact Section */
.contact {
    text-align: center;
    padding: 4rem 2rem;
    max-width: 600px;
    margin: 0 auto;
}

.contact p {
    color: var(--text-secondary);
    margin-bottom: 2rem;
    font-size: 1.1rem;
}

/* Footer */
footer {
    border-top: 1px solid var(--border);
    padding: 2rem;
    margin-top: 4rem;
}

.footer-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    max-width: 1100px;
    margin: 0 auto;
}

.footer-content p {
    color: var(--text-secondary);
    font-size: 0.9rem;
}

.social-links {
    display: flex;
    gap: 1.5rem;
}

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

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

/* Responsive */
@media (max-width: 768px) {
    .navbar {
        padding: 1rem;
    }

    .nav-links {
        gap: 1rem;
    }

    .footer-content {
        flex-direction: column;
        gap: 1rem;
        text-align: center;
    }
}

Your portfolio has gone from raw HTML to a polished, responsive website. Next up, you will add JavaScript to make the theme toggle button actually switch between light and dark mode.

Add JavaScript Interactivity

Your portfolio looks polished with a full layout and responsive styles. Now it is time to make it interactive.

A static page displays content, but users expect to interact with modern websites. In this step, you will add two features that make your portfolio feel alive: a dark/light theme toggle that remembers the visitor's preference, and smooth scrolling when navigation links are clicked.

In this step, get ready to:
  • Build a theme toggle that switches between dark and light mode.
  • Save the user's theme preference using localStorage.
  • Add smooth scroll behavior to your navigation links.
Implement the dark/light theme toggle

Your CSS already defines two sets of color variables: one in :root for light mode and one in [data-theme="dark"] for dark mode. The JavaScript toggle works by switching the data-theme attribute on the <html> element, which instantly swaps every color on the page.

  • In VS Code, click on script.js in the Explorer sidebar to open it.
  • Add the theme toggle setup code by pasting the following:
// Theme Toggle
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;

// Check for saved theme preference
const savedTheme = localStorage.getItem('theme') || 'light';
html.setAttribute('data-theme', savedTheme);
themeToggle.textContent = savedTheme === 'dark' ? '☀️' : '🌙';

What does this code do?

This block grabs two elements from the page and sets the initial theme state.

  • document.getElementById('theme-toggle') finds the moon button in your nav bar.
  • document.documentElement is a shortcut to the <html> element where data-theme lives.
  • localStorage.getItem('theme') checks if the user visited before and saved a preference. If not, it defaults to 'light'.
  • The last line sets the button emoji to a sun (if dark mode) or moon (if light mode) so the icon always shows what clicking will switch to.

What is localStorage?

localStorage is a browser storage mechanism that keeps data between page visits. It stores key-value pairs as strings. When your visitor returns tomorrow, the browser remembers their theme choice.

  • You will not see a change yet. The next chunk adds the click behavior that makes the button work.

Now you need to wire up the click event so the button actually toggles the theme.

  • Add the click event listener directly below the code you just pasted:
themeToggle.addEventListener('click', () => {
    const currentTheme = html.getAttribute('data-theme');
    const newTheme = currentTheme === 'dark' ? 'light' : 'dark';

    html.setAttribute('data-theme', newTheme);
    localStorage.setItem('theme', newTheme);
    themeToggle.textContent = newTheme === 'dark' ? '☀️' : '🌙';
});

What does this code do?

Every time the user clicks the toggle button, this listener fires and does three things:

  • Reads the current theme from the data-theme attribute, then flips it to the opposite value.
  • Saves the new choice into localStorage so it persists across page reloads.
  • Updates the button emoji so the user always knows what they will switch to next.
  • Save script.js.
  • Switch to your browser where Live Server is running and click the moon icon in the top-right corner of the nav bar.
  • You should see the entire page switch to a dark background with light text. The button should now show a sun emoji.
  • Refresh the page. Your theme choice should persist because it was saved to localStorage.

Theme not toggling?

  • Make sure the button in your HTML has id="theme-toggle". The JavaScript uses this exact ID to find the button.
  • Check that <script src="script.js"></script> is at the bottom of your index.html just before </body>. If the script loads before the HTML, it cannot find the button.
  • Open your browser's DevTools console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for red error messages.

Why isn't my theme toggle working in my portfolio site?

Add smooth scroll navigation

Clicking a navigation link currently jumps the page instantly to the target section. Smooth scrolling adds a polished animation that helps users understand where they are moving on the page.

  • In script.js, add the smooth scroll code below your theme toggle listener:
// Smooth Scroll Navigation
document.querySelectorAll('.nav-links a[href^="#"]').forEach(link => {
    link.addEventListener('click', (e) => {
        e.preventDefault();
        const targetId = link.getAttribute('href');
        const targetElement = document.querySelector(targetId);

        if (targetElement) {
            targetElement.scrollIntoView({ behavior: 'smooth' });
        }
    });
});

What does this code do?

This block finds every navigation link that points to a section on the same page (links starting with #) and overrides their default behavior.

  • e.preventDefault() stops the browser from jumping instantly.
  • link.getAttribute('href') reads the target section ID (like #about or #projects).
  • scrollIntoView({ behavior: 'smooth' }) tells the browser to animate the scroll to that section instead of teleporting.
  • Save script.js.
  • In your browser, scroll to the top of the page.
  • Click the Projects link in the nav bar. The page should glide smoothly down to the Projects section instead of jumping.

Page still jumping instantly?

  • Make sure you saved script.js. Unsaved changes will not take effect.
  • Verify that each nav link in your HTML has an href starting with # (like href="#about") and that the matching section has the same id (like id="about").
  • Check the DevTools console for errors. A typo in the selector string will silently fail.

Why isn't smooth scroll working on my nav links?

✔️ Awesome, I've got everything!

Great. Double check you have saved script.js before moving on.

ⓧ I'd like to double check the full code

Here is the complete script.js file. Compare your code against it:

// Theme Toggle
const themeToggle = document.getElementById('theme-toggle');
const html = document.documentElement;

// Check for saved theme preference
const savedTheme = localStorage.getItem('theme') || 'light';
html.setAttribute('data-theme', savedTheme);
themeToggle.textContent = savedTheme === 'dark' ? '☀️' : '🌙';

themeToggle.addEventListener('click', () => {
    const currentTheme = html.getAttribute('data-theme');
    const newTheme = currentTheme === 'dark' ? 'light' : 'dark';

    html.setAttribute('data-theme', newTheme);
    localStorage.setItem('theme', newTheme);
    themeToggle.textContent = newTheme === 'dark' ? '☀️' : '🌙';
});

// Smooth Scroll Navigation
document.querySelectorAll('.nav-links a[href^="#"]').forEach(link => {
    link.addEventListener('click', (e) => {
        e.preventDefault();
        const targetId = link.getAttribute('href');
        const targetElement = document.querySelector(targetId);

        if (targetElement) {
            targetElement.scrollIntoView({ behavior: 'smooth' });
        }
    });
});

Your portfolio now responds to user input. Next up, you will push everything to GitHub and deploy it as a live site anyone can visit.

Deploy to GitHub Pages

Your portfolio looks great locally. Now it needs a home on the internet where anyone with a link can see it.

In this step, you will push your code to GitHub and enable GitHub Pages to turn your repository into a live website. Every time you push a change, the site updates automatically.

In this step, get ready to:
  • Create a GitHub repository and push your code.
  • Enable GitHub Pages for free hosting.
  • Make a live update to confirm the deployment pipeline works.
Create a GitHub repository and push your code

GitHub Pages serves your website directly from a GitHub repository. For a personal site, the repository must be named after your GitHub username followed by .github.io.

  • Open your browser and go to https://github.com.
  • Sign in to your GitHub account.
  • Click the + icon in the top-right corner of the page.
  • Select New repository.
  • In the Repository name field, type 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.
  • Leave all other options unchecked (do not add a README, .gitignore, or license).
  • Click Create repository.

Why this specific repository name?

GitHub Pages uses the naming convention username.github.io to create a personal site at that same URL. If you name the repository anything else, the URL would include a project path instead of being a clean top-level address.

GitHub shows a quick setup page after creating the repository. Now you need to connect your local portfolio folder to this remote repository.

  • Switch back to VS Code and open the terminal (Ctrl+` on Windows or Cmd+` on macOS).
  • Stage all your files for a commit by running this command:
git add .
  • Create your first commit by running:
git commit -m "Initial portfolio site"
  • Rename your branch to main by running:
git branch -M main

Why rename the branch?

Some Git versions default to a branch named master instead of main. Running git branch -M main guarantees your branch is called main regardless of your Git configuration.

  • Connect your local repository to GitHub by adding the remote:
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/[[GITHUB_USERNAME="your-github-username"]].github.io.git
  • Push your code to GitHub by running:
git push -u origin main

What happens when you push?

The first time you push over HTTPS, Git will open a browser window asking you to sign in to GitHub. Follow the prompts to authorize access. Once authenticated, your credentials are cached and you will not be asked again.

You should see output ending with a line confirming that your main branch is set up to track the remote. This confirms your code is now on GitHub.

Push failed or asked for a password?

If Git asks for a password in the terminal instead of opening a browser, your Git Credential Manager may not be installed. Enter your GitHub username when prompted for a username, and a Personal Access Token (not your password) when prompted for a password. You can create a token at https://github.com/settings/tokens.

If you see an error about the remote already existing, you may have mistyped the URL. Run git remote remove origin and then re-run the git remote add command with the correct URL.

Help me debug my git push

Enable GitHub Pages and verify your live site

Your code is on GitHub, but the site is not live yet. You need to tell GitHub to serve the files as a website.

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

GitHub Pages will now build and deploy your site. This can take up to 10 minutes, but usually finishes within a couple of minutes.

  • Open a new browser tab and visit https://your-github-username.github.io

You should see your full portfolio site live on the internet. The navigation, hero section, project cards, and theme toggle all work exactly as they did locally.

Seeing a 404 page?

If you see a 404 error, the site may still be building. Wait a few minutes and refresh. You can check the build status on your repository's Actions tab on GitHub.

Also confirm that your repository name exactly matches the pattern username.github.io where username is your GitHub username (all lowercase).

Help me fix my 404 error on GitHub Pages

Make a change and see it go live

The real power of this setup is that every push to main automatically updates your live site. Try it now.

  • In VS Code, open index.html.
  • Find the subtitle line inside the hero section:
<p class="subtitle">I build things for the web.</p>
  • Change the text to something personal. For example:
<p class="subtitle">I build beautiful, responsive websites.</p>
  • Save index.html.
  • In the terminal, stage, commit, and push the change by running these commands:
git add .
git commit -m "Update tagline"
git push
  • Wait a couple of minutes, then refresh your live site in the browser.

Your updated tagline should now appear on the live site. This confirms your full deployment pipeline is working.

What is Git doing here?

Git tracks every version of your files. Each commit is a snapshot you can revert to if something breaks.

Pushing sends those snapshots to GitHub, which triggers a fresh deployment to your live site. Every future change follows the same three commands: git add ., git commit, git push.

Site not updating after push?

GitHub Pages can take up to 10 minutes to rebuild. If you have been waiting longer, check the Actions tab on your repository page for any build errors.

Make sure you ran git push after committing. Running git commit alone only saves the change locally.

Help me figure out why my site isn't updating

Your portfolio is live on the internet and you have a repeatable workflow for updates. Next up, take on the Secret Mission to add scroll-triggered animations to your project cards.

Secret mission

Scroll-Triggered Fade-In Animations

Your portfolio looks great, but it feels a bit static. What if your project cards appeared smoothly as the user scrolled down? In this secret mission, you will add scroll-triggered fade-in animations using a browser-native API. No libraries needed.

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.

Resources you used:

  • Local portfolio folder with your project files.
  • GitHub repository (username.github.io) with GitHub Pages enabled.
  • Live Server extension installed in VS Code.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Keep everything running

No action needed. Choose this if you plan to keep updating your portfolio site.

  • Your live site stays accessible at https://username.github.io.
  • Your local portfolio folder remains on your Desktop for future edits.
  • The Live Server extension stays available in VS Code for other projects.
  • VS Code and Git remain installed on your machine for future development.

Pause - I'll come back to this later

Since this project is entirely free, there is nothing to pause. Your GitHub Pages site will stay live at no cost, and your local files take up minimal space.

  • Leave everything as-is. GitHub Pages has no idle charges.
  • When you are ready to continue, open the portfolio 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 permanently deletes your live site and local files.

  • Delete the GitHub repository by going to your repository page on GitHub.
  • Click Settings in the top tab bar.
  • Scroll to the bottom of the page to the Danger Zone section.
  • Click Delete this repository and follow the confirmation prompts. This also removes your GitHub Pages site.

Delete your local project folder by running the following command in your terminal:

macOS

rm -rf ~/Desktop/portfolio

Windows

rmdir /s /q %USERPROFILE%\Desktop\portfolio

Uninstall the Live Server extension from VS Code:

  • Open the Extensions panel in VS Code by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Search for Live Server, click the extension, then click Uninstall.

Nice Work!

Nice Work!

You just built a personal portfolio website from scratch and deployed it live to the internet. Anyone with your URL can now see what you're capable of.

You've learned how to:

  • Build a responsive, single-page website using semantic HTML5, CSS custom properties, Flexbox, and CSS Grid.
  • Add a dark/light theme toggle that remembers the user's preference with localStorage.
  • Deploy a live site to GitHub Pages using Git version control (init, add, commit, push).
  • Add scroll-triggered fade-in animations using the Intersection Observer API.

Ready to quiz yourself?