Build a Personal Portfolio Page

Build a responsive portfolio page using HTML, CSS Flexbox, and CSS Grid.

Introduction

30 Second Summary

Your online presence is your first impression. Whether you're applying for jobs or showcasing side projects, a personal portfolio page tells the world who you are and what you can build.

In this project, you will build a fully responsive portfolio landing page from scratch using only HTML and CSS. You will combine CSS Flexbox and CSS Grid in a single layout to create a professional-looking site that adapts cleanly from desktop to mobile.

What You'll Build

A single-page portfolio website with a sticky navigation bar, a centered hero section, a skills display, a responsive project card grid, and a contact area.

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

  • A fully responsive portfolio page built from scratch that adapts to mobile, tablet, and desktop screens.
  • Practical experience combining Flexbox for one-dimensional alignment and Grid for two-dimensional card layouts in a real project.
  • A deployment-ready static site consisting of just two files that you can put live on the web.
  • Secret Mission: Refactor your CSS to use custom properties for theming and add polished hover animations with smooth scrolling.

Are there any prerequisites?

You should have a basic understanding of HTML tags and CSS selectors. If you know what a <div> is and how to write a CSS rule, you have everything you need to get started.

Set Up Your Project

Every website starts with a folder and two files: one for structure (HTML) and one for style (CSS). Setting up a project properly from the start means you can preview your changes in real time as you write code.

In this step, you'll create your project files, generate an HTML boilerplate with a single keystroke, and connect Live Server so your browser automatically refreshes whenever you save.

In this step, get ready to:
  • Create the portfolio project folder with index.html and style.css.
  • Generate HTML boilerplate using Emmet and link your stylesheet.
  • Start Live Server and confirm auto-refresh works.
Create your project folder and files

Before writing any code, you need a dedicated folder on your Desktop to keep your project organized.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (backtick, the key above Tab).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir portfolio
cd portfolio
  • Create the two project files by running:

macOS

touch index.html style.css

Windows

New-Item index.html, style.css
  • Confirm the files exist by running:
ls

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

Don't see both files?

Make sure you ran cd portfolio before creating the files. If ls shows nothing, run the file-creation command again from inside the portfolio folder.

On Windows, make sure you're using PowerShell (the default terminal in VS Code), not Command Prompt. Help me create my project files

Set up the HTML boilerplate

VS Code has built-in Emmet support that generates a complete HTML5 boilerplate from a single shortcut. This saves you from typing the same structure every time you start a new page.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the portfolio folder on your Desktop, then click Open.

Seeing a trust dialog?

VS Code may ask "Do you trust the authors of the files in this folder?" Click Yes, I trust the authors to continue. This is a standard security prompt for newly opened folders.

  • In the Explorer sidebar on the left, click index.html to open it.
  • Type ! (an exclamation mark) and press Tab.

What just happened?

Emmet recognized the ! abbreviation and generated a complete HTML5 boilerplate. This includes the DOCTYPE declaration, the <html> tag with a lang attribute, a <head> with meta tags, and an empty <body>.

  • Find this line in the <head> section:
<title>Document</title>
  • Replace Document with Your Name - Portfolio, so the line looks like this:
<title>Your Name - Portfolio</title>
  • Add a new line directly below the <title> tag and type this:
<link rel="stylesheet" href="style.css">

What does this link tag do?

This connects your HTML file to style.css. Any CSS rules you write in that file will automatically apply to the page.

  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Emmet not working?

Make sure the file is saved as index.html (check the bottom-right corner of VS Code shows "HTML" as the language mode). Emmet only triggers in recognized file types.

If pressing Tab inserts a tab character instead, try typing ! and then pressing Enter instead. Help me get Emmet working in VS Code

Start Live Server and confirm auto-refresh

Live Server launches a local development server that watches your files. When you save a change, it automatically refreshes your browser so you never have to manually reload.

  • In the Explorer sidebar, right-click index.html.
  • Select Open with Live Server.

Your default browser opens showing a blank page. This is expected because the <body> is empty.

  • Switch back to VS Code and make sure index.html is the active file.
  • Inside the <body> tags, add this line:
<h1>Hello World</h1>

What does this code do?

The <h1> tag creates a top-level heading. This is a quick test to confirm Live Server is connected and auto-refreshing your browser on every save.

  • Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Check your browser. You should see "Hello World" displayed on the page without manually refreshing.

Don't see Hello World in your browser?

Make sure you saved index.html after adding the <h1> tag. Unsaved files show a dot on the tab in VS Code.

If the browser tab never opened, try clicking Go Live in the bottom status bar of VS Code instead. This is an alternative way to start Live Server.

Help me get Live Server working

✔️ Awesome, I've got everything!

Great. Make sure you've saved index.html and your browser is showing "Hello World" with no manual refresh needed.

ⓧ I'd like to double check the full code

Your index.html should look exactly like this:

<!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>
  <h1>Hello World</h1>
</body>
</html>

Your style.css file should be empty for now.

Your project is ready to go. Next up, you'll build the full HTML structure for every section of your portfolio page.

Build the HTML Structure

Your Live Server is running and the browser refreshes every time you save. Now it is time to fill in the actual content of your portfolio page.

A well-structured HTML document uses semantic HTML elements like <header>, <main>, <section>, and <footer> to describe what each part of the page represents. Screen readers and search engines use these elements to understand your page layout automatically.

In this step, get ready to:
  • Build the navigation bar with links to each section.
  • Create the hero, about, skills, and projects sections inside a main element.
  • Add a contact section and footer to complete the page.
Create the header and navigation

Every portfolio page needs a navigation bar so visitors can jump to different sections. You will use a <nav> element inside a <header> to group your navigation links.

  • In index.html, find the line that says:
  <h1>Hello World</h1>
  • Delete that line and replace it with the following header and navigation code:
  <header>
    <nav class="navbar">
      <a href="#" class="logo">YourName</a>
      <ul class="nav-links">
        <li><a href="#about">About</a></li>
        <li><a href="#skills">Skills</a></li>
        <li><a href="#projects">Projects</a></li>
        <li><a href="#contact">Contact</a></li>
      </ul>
    </nav>
  </header>

  <main>

What does this code do?

  • The <header> wraps your site-wide banner content. Screen readers announce it as the page banner.
  • The <nav class="navbar"> tells browsers this is a navigation region. You will style it later using the navbar class.
  • The <ul class="nav-links"> is an unordered list of anchor links. Each href points to a section id you will create shortly.
  • The <main> element opens a wrapper for all your page content. You will close it at the end of the page.
  • Save index.html.
  • Check your browser. You should see "YourName" followed by the four link words (About, Skills, Projects, Contact) as plain text.

Don't see the navigation text?

Make sure you deleted the entire <h1>Hello World</h1> line before pasting. Leftover tags can break the layout.

Confirm your new code sits between the <body> and </body> tags.

Help me fix my navigation HTML

Add the hero, about, skills, and projects sections

The main content of your portfolio lives inside <section> elements. Each section gets an id so the navigation links can jump directly to it.

  • In index.html, place your cursor on the blank line directly after the <main> tag you just added.
  • Add the hero section by pasting this code:
    <section class="hero">
      <div class="hero-content">
        <h1>Hi, I'm <span class="highlight">Your Name</span></h1>
        <p>A web developer building clean, responsive websites.</p>
        <a href="#projects" class="cta-button">View My Work</a>
      </div>
    </section>

What does this code do?

  • The .hero section is the first thing visitors see. It holds your headline, tagline, and a call-to-action button.
  • The <span class="highlight"> wraps your name so you can color it differently with CSS later.
  • The .cta-button links to the projects section. It will become a styled button once you add CSS.
  • Save index.html and check your browser.
  • You should see your hero heading, tagline paragraph, and "View My Work" link text appear below the navigation.

Hero text not appearing?

Make sure you pasted the hero section after the <main> opening tag, not before it. The hero must be inside <main>.

Help me find where to put my hero section

  • Directly below the closing </section> tag of the hero, add the About and Skills sections by pasting this code:

    <section id="about" class="about">
      <h2>About Me</h2>
      <p>I'm a web developer passionate about creating beautiful, functional websites. I love turning ideas into reality using clean code and modern design principles.</p>
    </section>

    <section id="skills" class="skills">
      <h2>Skills</h2>
      <ul class="skills-list">
        <li>HTML5</li>
        <li>CSS3</li>
        <li>Flexbox</li>
        <li>CSS Grid</li>
        <li>Responsive Design</li>
        <li>Git</li>
      </ul>
    </section>

What does this code do?

  • Each <section> has an id matching the navigation links (#about, #skills). Clicking a nav link scrolls to that section.
  • The .skills-list is a <ul> with each skill as a list item. Later, CSS will turn these into styled pill-shaped tags.
  • Save index.html and check your browser.
  • You should see the "About Me" heading, a paragraph, the "Skills" heading, and a bulleted list of six skills.

Skills showing up as plain text without bullets?

Check that each skill is wrapped in its own <li> tag inside the <ul>. Missing list item tags will display everything as a single line.

Help me fix my skills list

  • Below the closing </section> tag of the skills section, add the Projects section by pasting this code:

    <section id="projects" class="projects">
      <h2>Projects</h2>
      <div class="project-grid">
        <article class="project-card">
          <div class="project-image"></div>
          <h3>Project One</h3>
          <p>A responsive landing page built with HTML and CSS.</p>
        </article>
        <article class="project-card">
          <div class="project-image"></div>
          <h3>Project Two</h3>
          <p>An interactive dashboard with modern layout techniques.</p>
        </article>
        <article class="project-card">
          <div class="project-image"></div>
          <h3>Project Three</h3>
          <p>A portfolio website showcasing creative work.</p>
        </article>
      </div>
    </section>

What does this code do?

  • The .project-grid div wraps three <article> cards. You will turn this into a CSS Grid layout later.
  • Each .project-card contains a placeholder <div class="project-image"> (empty for now), a title, and a description.
  • The <article> element marks self-contained content. It tells screen readers that each card is an independent piece.
  • Save index.html and check your browser.
  • You should see three project headings (Project One, Project Two, Project Three) each with a description paragraph.

Only seeing one or two project cards?

Check that all three <article> blocks are inside the <div class="project-grid"> wrapper. Each article must have its own closing </article> tag.

Help me fix my project cards

Add the contact section and footer

The final pieces of the page are a contact section for visitors to reach you and a footer with copyright information.

  • Below the closing </section> tag of the projects section, add the contact section, close <main>, and add the footer by pasting this code:

    <section id="contact" class="contact">
      <h2>Get In Touch</h2>
      <p>Interested in working together? Reach out!</p>
      <a href="mailto:your@email.com" class="cta-button">Say Hello</a>
    </section>
  </main>

  <footer>
    <p>&copy; 2026 Your Name. All rights reserved.</p>
  </footer>

What does this code do?

  • The contact section uses the same .cta-button class as the hero. This keeps styling consistent without duplicating CSS rules.
  • The </main> tag closes the wrapper you opened in the first substep. Everything between <main> and </main> is the primary page content.
  • The <footer> sits outside <main> because it is site-wide information, not the main content.
  • The &copy; is an HTML entity that renders as the © symbol.
  • Save index.html and check your browser.
  • You should see "Get In Touch", a paragraph, the "Say Hello" link, and a copyright line at the bottom of the page.

Seeing broken layout or missing content?

Scroll through your code and count your closing tags. You need one </section> for each <section>, one </main>, and one </footer>.

If content appears in the wrong order, check that each section is pasted after the previous one's closing tag, not nested inside it.

Help me fix my page structure

✔️ Awesome, I've got everything!

Double check that you have saved index.html. All your content should be visible in the browser as unstyled text.

ⓧ I'd like to double check the full code

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

<!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="#about">About</a></li>
        <li><a href="#skills">Skills</a></li>
        <li><a href="#projects">Projects</a></li>
        <li><a href="#contact">Contact</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <section class="hero">
      <div class="hero-content">
        <h1>Hi, I'm <span class="highlight">Your Name</span></h1>
        <p>A web developer building clean, responsive websites.</p>
        <a href="#projects" class="cta-button">View My Work</a>
      </div>
    </section>

    <section id="about" class="about">
      <h2>About Me</h2>
      <p>I'm a web developer passionate about creating beautiful, functional websites. I love turning ideas into reality using clean code and modern design principles.</p>
    </section>

    <section id="skills" class="skills">
      <h2>Skills</h2>
      <ul class="skills-list">
        <li>HTML5</li>
        <li>CSS3</li>
        <li>Flexbox</li>
        <li>CSS Grid</li>
        <li>Responsive Design</li>
        <li>Git</li>
      </ul>
    </section>

    <section id="projects" class="projects">
      <h2>Projects</h2>
      <div class="project-grid">
        <article class="project-card">
          <div class="project-image"></div>
          <h3>Project One</h3>
          <p>A responsive landing page built with HTML and CSS.</p>
        </article>
        <article class="project-card">
          <div class="project-image"></div>
          <h3>Project Two</h3>
          <p>An interactive dashboard with modern layout techniques.</p>
        </article>
        <article class="project-card">
          <div class="project-image"></div>
          <h3>Project Three</h3>
          <p>A portfolio website showcasing creative work.</p>
        </article>
      </div>
    </section>

    <section id="contact" class="contact">
      <h2>Get In Touch</h2>
      <p>Interested in working together? Reach out!</p>
      <a href="mailto:your@email.com" class="cta-button">Say Hello</a>
    </section>
  </main>

  <footer>
    <p>&copy; 2026 Your Name. All rights reserved.</p>
  </footer>
</body>
</html>

Why use semantic elements instead of plain divs?

Elements like <header>, <nav>, <main>, and <article> tell the browser what role each section plays. A <div> is a generic box with no meaning.

Screen readers map semantic elements to landmarks automatically. A visitor using assistive technology can jump straight to your <nav> or <main> without reading the whole page.

Your HTML skeleton is complete. Every section has its content, and the navigation links point to matching section IDs. Next, you will use CSS Flexbox to style the navigation bar and center the hero section.

Style the Navigation and Hero Section

Your HTML structure is solid, but right now it looks like a plain text document. A portfolio needs to feel professional the moment someone lands on the page.

In this step, you will use Flexbox to create a sticky navigation bar that stays visible as visitors scroll and a hero section that centers your name and tagline in the middle of the viewport.

In this step, get ready to:
  • Add a universal reset and base body styles to start from a clean slate.
  • Build a sticky navigation bar using Flexbox.
  • Create a centered hero section with a call-to-action button.
Add the universal reset and body styles

Every browser applies its own default margins and padding to elements. A universal reset removes those inconsistencies so your design looks the same everywhere.

  • Switch back to style.css in VS Code. Add the following code:
/* Universal reset - removes default browser margins and padding */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Base body styles - sets the font and text color for the whole page */
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #1a1a2e;
}

What does this code do?

The * selector targets every element on the page. Setting box-sizing: border-box makes padding and borders count toward an element's total width instead of adding extra size.

The font-family uses the system font stack. This picks the native font for each operating system, so the page loads instantly without downloading any external fonts.

  • Save style.css.
  • Check your browser. The text should look tighter now because all the default spacing is gone, and the font has changed to your system's native font.

No visible change in the browser?

Make sure you saved style.css. Live Server only picks up changes after a save.

Confirm that your <link> tag in index.html points to style.css (not styles.css or another filename).

Help me figure out why my CSS file isn't being applied to my HTML page.

Style the navigation bar with Flexbox

Flexbox is perfect for the navigation bar because it handles one-dimensional alignment. You need the logo on the left and the links on the right, all vertically centered.

  • Add the following code below your body styles in style.css:
/* Navigation */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background-color: #ffffff;
  border-bottom: 1px solid #e0e0e0;
  position: sticky;
  top: 0;
  z-index: 100;
}

/* Logo styling */
.logo {
  font-size: 1.5rem;
  font-weight: 700;
  color: #1a1a2e;
  text-decoration: none;
}

What does this code do?

display: flex turns the navbar into a flex container. justify-content: space-between pushes the logo to the far left and the links to the far right.

position: sticky with top: 0 keeps the navbar pinned to the top of the viewport as the user scrolls. The z-index: 100 ensures it stays above all other page content.

  • Save style.css.
  • In your browser, scroll down the page. The navigation bar should stay fixed at the top with the logo on the left side.

Navbar not sticking to the top?

Make sure position: sticky and top: 0 are both inside the .navbar block. If either is missing, the sticky behavior will not work.

Check that your page has enough content to actually scroll. If all the text fits on one screen, you will not see the sticky effect yet.

Help me debug why my sticky navbar is not working.

Style the navigation links

The navigation links still have default bullet points and underlines. Flexbox will arrange them in a horizontal row with clean spacing.

  • Add the following code below your .logo styles in style.css:
/* Navigation links - arranged horizontally with Flexbox */
.nav-links {
  display: flex;
  list-style: none;
  gap: 2rem;
}

/* Individual link styling with hover transition */
.nav-links a {
  text-decoration: none;
  color: #1a1a2e;
  font-weight: 500;
  transition: color 0.3s;
}

.nav-links a:hover {
  color: #4361ee;
}

What does this code do?

list-style: none removes the bullet points. display: flex with gap: 2rem lays the links out in a row with even spacing between them.

The transition: color 0.3s property makes the color change animate smoothly over 0.3 seconds when the user hovers, instead of snapping instantly.

  • Save style.css.
  • In your browser, hover over the navigation links. They should change to blue with a smooth transition. The bullet points and underlines are gone.

Links still have underlines or bullet points?

Double check that text-decoration: none is inside .nav-links a (not just .nav-links). The underline belongs to the <a> tags, so you need to target those specifically.

Help me figure out why my nav link styles are not being applied correctly.

Style the hero section

The hero section is the first thing visitors see below the navbar. It needs to fill most of the screen and center its content both vertically and horizontally.

  • Add the following code below your .nav-links a:hover styles in style.css:
/* Hero Section */
.hero {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 80vh;
  background-color: #f8f9fa;
  text-align: center;
  padding: 2rem;
}

/* Hero heading */
.hero h1 {
  font-size: 3rem;
  margin-bottom: 1rem;
}

/* Highlighted name color */
.highlight {
  color: #4361ee;
}

/* Hero paragraph spacing */
.hero p {
  font-size: 1.25rem;
  color: #555;
  margin-bottom: 2rem;
}

What does this code do?

The combination of display: flex, align-items: center, and justify-content: center centers content in both directions. This is the classic Flexbox centering trick.

min-height: 80vh means the hero takes up at least 80% of the browser window height. The .highlight class colors just your name in the heading blue.

  • Save style.css.
  • In your browser, you should see the hero section filling most of the screen with a light gray background. Your name appears large and centered, with the highlighted text in blue.

Hero content not centered vertically?

Flexbox centering only works when the container is taller than its content. Confirm that min-height: 80vh is present inside your .hero block. Without it, the section collapses to fit its content and there is nothing to center within.

Help me understand why my Flexbox centering is not working on the hero section.

  • Now add the call-to-action button styles below your .hero p styles:
/* Call-to-action button */
.cta-button {
  display: inline-block;
  padding: 0.75rem 2rem;
  background-color: #4361ee;
  color: #ffffff;
  text-decoration: none;
  border-radius: 4px;
  font-weight: 600;
  transition: background-color 0.3s;
}

/* Button hover state - slightly darker shade */
.cta-button:hover {
  background-color: #3a56d4;
}

What does this code do?

display: inline-block lets you add padding and dimensions to the link while keeping it inline with text flow. The border-radius: 4px gives the button slightly rounded corners.

The transition: background-color 0.3s creates a smooth color shift when hovering, making the button feel interactive and polished.

  • Save style.css.
  • In your browser, you should see a blue button labeled "View My Work" below the tagline. Hover over it to see the color darken smoothly.

Button not appearing as a styled rectangle?

Check that your HTML has class="cta-button" on the <a> tag inside the hero section. Without the class, the CSS selector will not match.

Also confirm that display: inline-block is present. Without it, the padding will not push outward to create the button shape.

Help me figure out why my CTA button does not look right.

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css before moving on.

ⓧ I'd like to double check the full code

Here is the complete style.css file at the end of this step:

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

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #1a1a2e;
}

/* Navigation */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background-color: #ffffff;
  border-bottom: 1px solid #e0e0e0;
  position: sticky;
  top: 0;
  z-index: 100;
}

.logo {
  font-size: 1.5rem;
  font-weight: 700;
  color: #1a1a2e;
  text-decoration: none;
}

.nav-links {
  display: flex;
  list-style: none;
  gap: 2rem;
}

.nav-links a {
  text-decoration: none;
  color: #1a1a2e;
  font-weight: 500;
  transition: color 0.3s;
}

.nav-links a:hover {
  color: #4361ee;
}

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

.hero h1 {
  font-size: 3rem;
  margin-bottom: 1rem;
}

.highlight {
  color: #4361ee;
}

.hero p {
  font-size: 1.25rem;
  color: #555;
  margin-bottom: 2rem;
}

.cta-button {
  display: inline-block;
  padding: 0.75rem 2rem;
  background-color: #4361ee;
  color: #ffffff;
  text-decoration: none;
  border-radius: 4px;
  font-weight: 600;
  transition: background-color 0.3s;
}

.cta-button:hover {
  background-color: #3a56d4;
}

Your navigation and hero section are looking sharp. Next up, you will use CSS Grid to create a responsive project card layout and finish styling the rest of the page.

Style the Content Sections and Project Grid

In the last step, you gave the navigation bar and hero section their layout and polish. Now it's time to style the sections that hold your actual content.

This step introduces CSS Grid for the project card layout. While Flexbox handles one-dimensional rows (like your nav links), Grid excels at two-dimensional layouts where you need control over both rows and columns simultaneously.

In this step, get ready to:
  • Style the About and Skills sections with Flexbox.
  • Build a responsive project card grid with CSS Grid.
  • Finish the Contact section and footer styles.
Style the About and Skills sections

The About and Skills sections need spacing, readable line widths, and visual styling that makes each section feel distinct.

  • In style.css, add the About section styles below the .cta-button:hover rule:
/* About Section */
.about {
  padding: 4rem 2rem;
  max-width: 800px;
  margin: 0 auto;
}

.about h2 {
  margin-bottom: 1rem;
  color: #1a1a2e;
}

.about p {
  color: #555;
  font-size: 1.1rem;
}

What does this code do?

  • max-width: 800px prevents the text from stretching too wide on large screens. Long lines of text are hard to read.
  • margin: 0 auto centers the section horizontally within the page. The 0 removes top/bottom margin and auto splits the remaining space evenly on left and right.
  • Save style.css.
  • Your browser should show the About heading and paragraph centered with comfortable spacing on the left and right.

Now add the Skills section styles. The skills will display as pill-shaped tags using Flexbox.

  • Add the Skills section styles below the .about p rule:
/* Skills Section */
.skills {
  padding: 4rem 2rem;
  background-color: #f8f9fa;
}

.skills h2 {
  text-align: center;
  margin-bottom: 2rem;
}

.skills-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem;
  list-style: none;
  max-width: 800px;
  margin: 0 auto;
}

.skills-list li {
  background-color: #ffffff;
  padding: 0.5rem 1.5rem;
  border-radius: 20px;
  border: 2px solid #4361ee;
  color: #4361ee;
  font-weight: 500;
}

What does this code do?

  • flex-wrap: wrap lets the skill items flow onto a new line when they run out of horizontal space.
  • justify-content: center centers the tags within the row so they look balanced.
  • border-radius: 20px creates the rounded pill shape on each skill tag.
  • gap: 1rem adds consistent spacing between each tag without needing margin on individual items.
  • Save style.css and check your browser.
  • You should see your skills displayed as rounded blue-bordered pill tags in a centered, wrapping row against a light gray background.

Skills showing as a plain list?

Make sure list-style: none is inside your .skills-list rule. Without it, the default bullet points still appear.

Also check that display: flex is set on .skills-list (the <ul>), not on .skills-list li (the individual items).

Still stuck? Help me debug my skills section styling.

Build the responsive project grid

Your project cards need a layout that automatically adjusts from three columns on wide screens to one column on narrow screens. This is where CSS Grid shines.

  • Add the project grid styles below the .skills-list li rule:
/* Projects Section */
.projects {
  padding: 4rem 2rem;
  max-width: 1000px;
  margin: 0 auto;
}

.projects h2 {
  text-align: center;
  margin-bottom: 2rem;
}

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

.project-card {
  background-color: #ffffff;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s;
}

.project-card:hover {
  transform: translateY(-4px);
}

What does this code do?

The key line is grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)). This tells the browser to create as many columns as will fit, with each column being at least 280px wide. When the screen is too narrow for multiple columns, the cards automatically stack.

The overflow: hidden on .project-card clips the image placeholder to the card's rounded corners. The transform: translateY(-4px) makes cards lift slightly on hover for a tactile feel.

Why Grid instead of Flexbox here?

Flexbox is content-first. It distributes items along a single axis and lets them dictate their own size. Grid is layout-first. It defines the column structure and places items into it.

For a card layout where you want uniform column widths regardless of content length, Grid gives you that control in one line. With Flexbox, you would need percentage widths and manual wrapping calculations.

  • Save style.css and check your browser.
  • You should see three white cards side by side with rounded corners and a subtle shadow. Try resizing your browser window narrower. The cards reflow from 3 columns to 2, then to 1.

Cards not appearing in a grid?

Make sure display: grid is on .project-grid (the wrapper div), not on .project-card (the individual cards).

Check that grid-template-columns is spelled exactly right. A typo in this property silently fails and the cards stack in a single column.

Need help? Help me debug my CSS Grid project card layout.

Style the project cards, Contact section, and footer

The cards need internal styling for the image placeholder and text. The Contact section and footer round out the page.

  • Add the remaining styles below the .project-card:hover rule:
.project-image {
  height: 180px;
  background-color: #e0e7ff;
}

.project-card h3 {
  padding: 1rem 1rem 0.5rem;
  color: #1a1a2e;
}

.project-card p {
  padding: 0 1rem 1rem;
  color: #555;
}

/* Contact Section */
.contact {
  padding: 4rem 2rem;
  text-align: center;
  background-color: #f8f9fa;
}

.contact h2 {
  margin-bottom: 1rem;
}

.contact p {
  color: #555;
  margin-bottom: 2rem;
}

/* Footer */
footer {
  padding: 2rem;
  text-align: center;
  background-color: #1a1a2e;
  color: #ffffff;
}

What does this code do?

  • .project-image gives each card a colored placeholder block at the top. In a real portfolio, you would replace this with actual screenshots.
  • The Contact section reuses the same .cta-button class from the hero, so the "Say Hello" button is already styled.
  • The footer uses the same dark color as the body text (#1a1a2e) for its background, with white text for contrast.
  • Save style.css and check your browser.
  • Each project card now has a light purple placeholder image at the top. Scroll down to see the centered Contact section with a blue "Say Hello" button, and a dark footer at the bottom of the page.

Footer not showing a dark background?

The CSS selector is just footer (no dot prefix). This targets the HTML <footer> element directly. Make sure you did not write .footer with a dot, since that targets a class name.

Something else not right? Help me figure out what's wrong with my page styling.

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css before moving on.

ⓧ I'd like to double check the full code

Here is the complete style.css up to this point:

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

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #1a1a2e;
}

/* Navigation */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background-color: #ffffff;
  border-bottom: 1px solid #e0e0e0;
  position: sticky;
  top: 0;
  z-index: 100;
}

.logo {
  font-size: 1.5rem;
  font-weight: 700;
  color: #1a1a2e;
  text-decoration: none;
}

.nav-links {
  display: flex;
  list-style: none;
  gap: 2rem;
}

.nav-links a {
  text-decoration: none;
  color: #1a1a2e;
  font-weight: 500;
  transition: color 0.3s;
}

.nav-links a:hover {
  color: #4361ee;
}

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

.hero h1 {
  font-size: 3rem;
  margin-bottom: 1rem;
}

.highlight {
  color: #4361ee;
}

.hero p {
  font-size: 1.25rem;
  color: #555;
  margin-bottom: 2rem;
}

.cta-button {
  display: inline-block;
  padding: 0.75rem 2rem;
  background-color: #4361ee;
  color: #ffffff;
  text-decoration: none;
  border-radius: 4px;
  font-weight: 600;
  transition: background-color 0.3s;
}

.cta-button:hover {
  background-color: #3a56d4;
}

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

.about h2 {
  margin-bottom: 1rem;
  color: #1a1a2e;
}

.about p {
  color: #555;
  font-size: 1.1rem;
}

/* Skills Section */
.skills {
  padding: 4rem 2rem;
  background-color: #f8f9fa;
}

.skills h2 {
  text-align: center;
  margin-bottom: 2rem;
}

.skills-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem;
  list-style: none;
  max-width: 800px;
  margin: 0 auto;
}

.skills-list li {
  background-color: #ffffff;
  padding: 0.5rem 1.5rem;
  border-radius: 20px;
  border: 2px solid #4361ee;
  color: #4361ee;
  font-weight: 500;
}

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

.projects h2 {
  text-align: center;
  margin-bottom: 2rem;
}

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

.project-card {
  background-color: #ffffff;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s;
}

.project-card:hover {
  transform: translateY(-4px);
}

.project-image {
  height: 180px;
  background-color: #e0e7ff;
}

.project-card h3 {
  padding: 1rem 1rem 0.5rem;
  color: #1a1a2e;
}

.project-card p {
  padding: 0 1rem 1rem;
  color: #555;
}

/* Contact Section */
.contact {
  padding: 4rem 2rem;
  text-align: center;
  background-color: #f8f9fa;
}

.contact h2 {
  margin-bottom: 1rem;
}

.contact p {
  color: #555;
  margin-bottom: 2rem;
}

/* Footer */
footer {
  padding: 2rem;
  text-align: center;
  background-color: #1a1a2e;
  color: #ffffff;
}

Every section of your portfolio is now styled and visible. Next up, you will add a media query to handle the mobile-specific layout adjustments that CSS Grid does not cover on its own.

Make It Responsive

Your portfolio looks great on a desktop screen, but try dragging your browser window narrower. The navigation bar gets cramped, the hero text overflows, and the layout starts to break.

A media query lets you write CSS rules that only activate at certain screen widths. In this step, you will add a single media query that handles the layout adjustments your Grid's auto-fit pattern cannot cover on its own.

In this step, get ready to:
  • Add a media query that targets screens 768px and below.
  • Adjust the navigation, hero text, and project grid for mobile.
  • Test your responsive design in Chrome DevTools at multiple viewport widths.
Add the media query and fix the navbar for mobile

On small screens, a horizontal navbar with the logo on the left and links on the right gets too crowded. Stacking them vertically solves this.

  • In style.css, scroll to the very bottom of the file (below the footer styles).
  • Add this media query block:
/* Responsive Design */
@media (max-width: 768px) {
  .navbar {
    flex-direction: column;
    gap: 1rem;
  }

  .nav-links {
    gap: 1rem;
  }
}

What does this code do?

The @media (max-width: 768px) wrapper means everything inside only applies when the browser window is 768 pixels wide or narrower (a typical tablet breakpoint).

Setting flex-direction: column on .navbar flips the layout from a horizontal row to a vertical stack. The gap: 1rem adds spacing between the logo and the links. Reducing .nav-links gap from 2rem to 1rem keeps the link list compact on smaller screens.

  • Save style.css.
  • Drag your browser window to about half the screen width. You should see the logo centered above the navigation links instead of sitting beside them.

Navigation still in a row?

Make sure your media query is at the very bottom of style.css, after all other styles. If it appears above the .navbar rules, the later non-media styles override it.

Also confirm you typed 768px (not 768 without the unit). CSS requires the unit on non-zero values.

help me figure out why my media query isn't working

Adjust the hero text and project grid for mobile

A 3rem heading looks great on desktop but overwhelms a phone screen. The project grid's auto-fit handles most reflows, but adding an explicit single-column rule guarantees it works even if the container is slightly wider than 280px.

  • Inside the same @media (max-width: 768px) block, find the closing } of the .nav-links rule.
  • Add the following rules below it (but still inside the media query's outer }):
  .hero h1 {
    font-size: 2rem;
  }

  .hero p {
    font-size: 1rem;
  }

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

What does this code do?

Reducing .hero h1 from 3rem to 2rem and .hero p from 1.25rem to 1rem prevents the hero text from overflowing on narrow viewports.

Setting grid-template-columns: 1fr on .project-grid forces a single-column layout. This is a safety net for cases where the viewport is slightly above 280px but not wide enough for two comfortable cards.

  • Save style.css.
  • With your browser still at half width, scroll down. You should see the project cards stacked in a single column and the hero heading noticeably smaller than before.

Project cards still in multiple columns?

Check that grid-template-columns: 1fr is inside the @media block's braces, not outside them. A missing or extra } is the most common cause.

help me debug my responsive grid

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css before moving on.

ⓧ I'd like to double check the full code

Here is the complete style.css including the new media query at the bottom:

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

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  line-height: 1.6;
  color: #1a1a2e;
}

/* Navigation */
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background-color: #ffffff;
  border-bottom: 1px solid #e0e0e0;
  position: sticky;
  top: 0;
  z-index: 100;
}

.logo {
  font-size: 1.5rem;
  font-weight: 700;
  color: #1a1a2e;
  text-decoration: none;
}

.nav-links {
  display: flex;
  list-style: none;
  gap: 2rem;
}

.nav-links a {
  text-decoration: none;
  color: #1a1a2e;
  font-weight: 500;
  transition: color 0.3s;
}

.nav-links a:hover {
  color: #4361ee;
}

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

.hero h1 {
  font-size: 3rem;
  margin-bottom: 1rem;
}

.highlight {
  color: #4361ee;
}

.hero p {
  font-size: 1.25rem;
  color: #555;
  margin-bottom: 2rem;
}

.cta-button {
  display: inline-block;
  padding: 0.75rem 2rem;
  background-color: #4361ee;
  color: #ffffff;
  text-decoration: none;
  border-radius: 4px;
  font-weight: 600;
  transition: background-color 0.3s;
}

.cta-button:hover {
  background-color: #3a56d4;
}

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

.about h2 {
  margin-bottom: 1rem;
  color: #1a1a2e;
}

.about p {
  color: #555;
  font-size: 1.1rem;
}

/* Skills Section */
.skills {
  padding: 4rem 2rem;
  background-color: #f8f9fa;
}

.skills h2 {
  text-align: center;
  margin-bottom: 2rem;
}

.skills-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem;
  list-style: none;
  max-width: 800px;
  margin: 0 auto;
}

.skills-list li {
  background-color: #ffffff;
  padding: 0.5rem 1.5rem;
  border-radius: 20px;
  border: 2px solid #4361ee;
  color: #4361ee;
  font-weight: 500;
}

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

.projects h2 {
  text-align: center;
  margin-bottom: 2rem;
}

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

.project-card {
  background-color: #ffffff;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s;
}

.project-card:hover {
  transform: translateY(-4px);
}

.project-image {
  height: 180px;
  background-color: #e0e7ff;
}

.project-card h3 {
  padding: 1rem 1rem 0.5rem;
  color: #1a1a2e;
}

.project-card p {
  padding: 0 1rem 1rem;
  color: #555;
}

/* Contact Section */
.contact {
  padding: 4rem 2rem;
  text-align: center;
  background-color: #f8f9fa;
}

.contact h2 {
  margin-bottom: 1rem;
}

.contact p {
  color: #555;
  margin-bottom: 2rem;
}

/* Footer */
footer {
  padding: 2rem;
  text-align: center;
  background-color: #1a1a2e;
  color: #ffffff;
}

/* Responsive Design */
@media (max-width: 768px) {
  .navbar {
    flex-direction: column;
    gap: 1rem;
  }

  .nav-links {
    gap: 1rem;
  }

  .hero h1 {
    font-size: 2rem;
  }

  .hero p {
    font-size: 1rem;
  }

  .project-grid {
    grid-template-columns: 1fr;
  }
}
Test your responsive layout in Chrome DevTools

Dragging your browser window is a rough test. Chrome DevTools gives you precise control over the viewport width so you can confirm your page works at exact breakpoints.

  • Open Chrome DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
  • Click the device toolbar icon (the small phone-and-tablet icon in the top-left of the DevTools panel).
  • In the dimensions dropdown at the top, select Responsive.
  • Set the width to 375 (a typical mobile width). Scroll through the entire page and confirm the navigation is stacked, the hero text fits, and the project cards are in a single column.
  • Change the width to 768 (the exact breakpoint). Verify the media query is still active at this width.
  • Change the width to 1200 (desktop). Confirm the navigation returns to a horizontal row and the project cards display in multiple columns.

What should I see at each width?

  • 375px (mobile): logo centered above nav links, smaller heading, single-column project cards.
  • 768px (tablet): same stacked layout (the breakpoint is inclusive at 768px).
  • 1200px (desktop): horizontal navbar, full-size hero text, multi-column project grid.

Page looks the same at all sizes?

Make sure you are resizing the viewport in DevTools (the blue dimension bar at the top), not the browser window itself. The device toolbar must be active (the icon appears highlighted in blue).

Also confirm your index.html has the viewport meta tag: <meta name="viewport" content="width=device-width, initial-scale=1.0">. Without it, mobile browsers ignore your media queries.

help me debug why my responsive styles aren't showing in DevTools

Secret mission

Add CSS Custom Properties and Polished Interactions

Your portfolio works, but it's not easy to maintain or theme. What if you wanted to change your accent color from blue to green? You'd have to hunt through the entire stylesheet replacing every instance of #4361ee. Refactor your CSS to use custom properties, add smooth scrolling, and create polished hover animations that make your portfolio feel professional.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your files for future editing, or delete them entirely.

Resources you used:

  • The portfolio folder on your Desktop (contains index.html and style.css).
  • Live Server running in VS Code.

Keep everything running

No action needed. Choose this if you're still actively building or plan to customize your portfolio further.

  • Your portfolio folder stays on your Desktop, ready to edit anytime.
  • Live Server keeps running until you close VS Code or click Port: 5500 in the status bar to stop it.
  • You can deploy these two files to GitHub Pages, Netlify, or Vercel whenever you're ready.

Pause - I'll come back to this later

Stop the Live Server process to free up the port, but keep your files for later.

  • In VS Code, click Port: 5500 in the bottom status bar to stop Live Server.
  • Close VS Code.
  • Your portfolio folder remains on your Desktop. Reopen it in VS Code and restart Live Server when you're ready to continue.

Delete - I don't want to use this again

Remove all project files and stop the running server.

  • In VS Code, click Port: 5500 in the bottom status bar to stop Live Server.
  • Close VS Code.
  • Delete the portfolio folder from your Desktop by running this command in your terminal:

macOS

rm -rf ~/Desktop/portfolio

Windows

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

Nice Work!

Nice Work!

You just built a fully responsive personal portfolio page from scratch using nothing but HTML and CSS. Two files, zero dependencies, and a layout that adapts beautifully from desktop to mobile.

You've learned how to:

  • Structure a complete single-page site with semantic HTML5 elements (header, nav, main, section, article, footer) that map to accessibility landmarks.
  • Use CSS Flexbox for one-dimensional alignment (navigation bar, hero centering, skill tags) and CSS Grid with repeat(auto-fit, minmax()) for a responsive project card layout.
  • Add responsive design with a media query targeting 768px, creating a deployment-ready static site with just two files.
  • Secret Mission: refactored colors into CSS custom properties, added smooth scrolling, and created polished hover animations with transforms and box-shadows.

Ready to quiz yourself?