Build a Responsive Portfolio
Build a responsive portfolio with filters and publish it on GitHub Pages.
Introduction
30 Second Summary
Potential clients and hiring managers can form an impression before they ever speak with you. A page that feels clear on both a laptop and a phone makes that first look count.
In this project, you will build a one-page portfolio with semantic HTML, responsive CSS, and interactive JavaScript. You will publish it with GitHub Pages so visitors can open it from a public URL.
What You'll Build
You will share a polished public portfolio that adapts to each screen while its project filters reshape the gallery instantly.
By the end of this project, you'll have:
- A shareable portfolio at a public GitHub Pages URL, with navigation to your About, Projects, and Contact sections.
- Working project filters that switch the gallery between all, web, and design work.
- A responsive layout that displays three columns on wide screens while stacking cleanly on narrow screens.
- Secret Mission: Add a light and dark theme switch that remembers each visitor's choice across reloads.
Are there any prerequisites?
You need a Mac with Visual Studio Code plus basic familiarity with HTML, CSS, and JavaScript.
A GitHub account on GitHub Free supports the public-repository path at $0.
Before We Start
Before you start building, take a moment to define who your portfolio serves and the first impression it should create. This gives every content choice a clear purpose.
Set Up the Portfolio Workspace
Your portfolio needs a home before its content can take shape. A three-file workspace keeps each responsibility separate.
HTML provides the page structure. CSS controls its presentation.
JavaScript adds behavior. Visual Studio Code keeps these files together while its Integrated Browser provides an immediate preview.
In this step, get ready to:
- Create the local portfolio-site workspace.
- Add a linked starter document with empty support files.
- Preview the starter page in the Integrated Browser.
Create the workspace
A workspace is a folder that Visual Studio Code treats as one project. Your portfolio-site workspace will contain every local file for the portfolio.
- Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
- Type Visual Studio Code and press Enter to open it.
You should see the Visual Studio Code welcome window.
- Click File in the top menu.
- Select Open Folder....
You should see a folder picker where you can choose the workspace location.
- Select Desktop in the folder picker.
- Select New Folder.
You should see a field where you can name the new folder.
- Enter portfolio-site as the folder name.
- Select Open to open the new folder in Visual Studio Code.
You should see portfolio-site at the top of the Explorer view.
Visual Studio Code may ask whether you trust this folder before enabling every workspace feature. This folder contains only files you create, so granting trust here is safe.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
The Explorer view is now ready to hold your project files.
- Select the Explorer view in the Activity Bar.
- Select the New File... button in the Explorer view.
You should see a filename field inside the portfolio-site folder.
- Enter index.html and press Enter.
You should see index.html in the Explorer view with its blank editor open.
- Select the New File... button again.
- Enter styles.css and press Enter.
You should now see styles.css beside index.html in the Explorer view.
- Select the New File... button again.
- Enter script.js and press Enter.
The Explorer now lists index.html, styles.css, and script.js directly inside portfolio-site.
Add a temporary starter page
The starter document proves that your workspace can render HTML before you build the full portfolio. Its linked stylesheet and script remain empty at this checkpoint.
- Select index.html in the Explorer view.
- Paste this complete starter document into the blank editor:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Portfolio Setup</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<p>Portfolio setup ready</p>
</body>
</html>
What does this code do?
- The <!doctype html> declaration identifies this file as an HTML document.
- The <link rel="stylesheet" href="styles.css"> element connects the empty stylesheet to the page.
- The <script src="script.js" defer></script> element connects the empty script. The defer attribute waits until the document has been parsed before running the script.
- The body contains Portfolio setup ready as the visible signal that the document loaded.
- Save index.html by pressing Cmd+S on macOS or Ctrl+S on Windows.
- Confirm the index.html editor tab no longer shows an unsaved indicator.
Seeing an HTML warning?
- Check that the file is named exactly index.html.
- Compare every opening tag with its matching closing tag.
- Ask for help comparing the starter document: Help me find the syntax problem in my starter HTML.
✔️ Awesome, I've got everything!
Your starter document is saved. Keep styles.css and script.js empty for this checkpoint.
ⓧ I'd like to double check the full code
Compare index.html with this exact checkpoint version.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Portfolio Setup</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<p>Portfolio setup ready</p>
</body>
</html>
The styles.css file should be completely empty.
The script.js file should be completely empty.
Preview the starter
The Integrated Browser renders index.html inside Visual Studio Code. This gives you a quick feedback loop between the file and its visible result.
Before you open the preview, what text do you expect the page to show?
- Right-click index.html in the Explorer view.
- Select Open in Integrated Browser.
You'll see Portfolio setup ready in the Integrated Browser. That is your first visible win: your local index.html is rendering inside Visual Studio Code.
Preview not showing the starter text?
- Save index.html if the Integrated Browser shows older content.
- Confirm index.html sits directly inside portfolio-site.
- Ask for help checking the preview: Help me fix my Integrated Browser preview.
Your three-file workspace now produces a working browser preview. Next, you'll replace the starter message with the complete portfolio structure.
Build the Portfolio Structure
Your starter page in Visual Studio Code proves that your three files connect in the browser. However, one sentence cannot show visitors who you are or what you can do.
Semantic HTML gives each part of the portfolio a clear purpose. You will now build the complete content structure in index.html.
Once the content is visible, you can test how much the structure accomplishes on its own.
In this step, get ready to:
- Replace the starter page with the complete semantic portfolio structure.
- Map the sample copy to your own portfolio details.
- Test the navigation links and project controls in the browser.
Replace the starter with semantic sections
A semantic page uses elements such as header, nav, main, and section to describe the purpose of its content. Start with the document foundation and navigation.
- Select index.html in the Explorer sidebar.
- Press Cmd+A (macOS) or Ctrl+A (Windows) to select the starter code.
- Replace the selected starter with this document shell by pasting the code below:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta
name="description"
content="A personal portfolio featuring selected web and design projects."
>
<title>Alex Morgan | Portfolio</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main-content">Skip to main content</a>
<header class="site-header">
<a class="logo" href="#top" aria-label="Alex Morgan home">AM</a>
<nav aria-label="Primary navigation">
<a href="#about">About</a>
<a href="#projects">Projects</a>
<a href="#contact">Contact</a>
</nav>
</header>
</body>
</html>
What does this structure do?
- The viewport metadata prepares the page to match the width of a mobile device.
- The defer attribute waits until the document is parsed before running script.js.
- The header provides a logo link and three navigation links.
- The skip link gives keyboard users a direct route to the main content.
- Save index.html.
- Return to the Integrated Browser from earlier.
- Refresh the preview.
Your document foundation is now visible. You should see the skip link followed by AM, About, Projects, and Contact.
Header missing from the preview?
- Confirm that you replaced the contents of index.html instead of one of the empty files.
- Check that the opening and closing body tags remain in the file.
- Ask for help with the document shell.
The header gives visitors a route through the page. The hero now needs to tell them whose work they are viewing.
- Find the closing </header> tag in index.html.
- Insert this main hero structure directly below it:
<main id="main-content">
<section class="hero" id="top">
<p class="eyebrow">Front-end developer</p>
<h1>Hi, I'm Alex. I build clear, friendly digital experiences.</h1>
<p class="hero-copy">
I turn thoughtful ideas into responsive websites that are simple to
use and easy to understand.
</p>
<a class="button" href="#projects">See my work</a>
</section>
</main>
How does the hero work?
- The main-content identifier gives the skip link its destination.
- The top identifier gives the logo a destination at the start of the portfolio.
- The hero introduces the sample role and value statement.
- The See my work link points visitors toward the projects section.
- Save index.html.
- Refresh the Integrated Browser preview.
The portfolio now opens with a role, introduction, and link to the upcoming project gallery.
Hero text not appearing?
- Make sure the new main element sits between </header> and </body>.
- Check that the hero has both an opening section tag and a closing </section> tag.
- Ask for help locating an unclosed HTML element.
The introduction makes the first impression. The About section supports it with a short biography and a focused skills list.
- Find the closing </main> tag.
- Insert this About section on a new line directly above it:
<section class="about" id="about">
<div>
<p class="eyebrow">About</p>
<h2>Design-minded development</h2>
</div>
<div>
<p>
I enjoy combining clean code with practical design. My focus is on
accessible interfaces, responsive layouts, and useful interactions.
</p>
<ul class="skill-list" aria-label="Skills">
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
<li>Responsive design</li>
</ul>
</div>
</section>
Why separate About and Skills?
The biography explains how the sample developer approaches their work. The list turns broad experience into skills that visitors can scan quickly.
The about identifier also gives the header link a precise destination.
- Save index.html.
- Refresh the Integrated Browser preview.
- Select the About navigation link.
Your page now moves to the About section. You should see the biography followed by four skills.
About link going nowhere?
- Confirm that the navigation link uses href="#about".
- Confirm that the section uses id="about".
- Ask for help matching the link and section identifier.
A portfolio needs evidence of the work behind its claims. The project section provides a heading, filter area, gallery container, and live status region.
- Find the closing </main> tag again.
- Insert this project-section shell directly above it:
<section id="projects">
<div class="section-heading">
<div>
<p class="eyebrow">Selected work</p>
<h2>Projects</h2>
</div>
<div class="filters" aria-label="Filter projects">
</div>
</div>
<div class="project-grid">
</div>
<p id="filter-status" class="sr-only" aria-live="polite"></p>
</section>
What does the project shell prepare?
- The projects identifier connects the section to navigation links elsewhere on the page.
- The filters container groups the category controls.
- The project-grid container holds the project cards.
- The filter-status element provides a place for later filter updates.
- Save index.html.
- Refresh the Integrated Browser preview.
The page now includes a Selected work label and Projects heading below the About section.
Projects heading missing?
- Check that the project section sits inside main.
- Check that the project section appears before the closing </main> tag.
- Ask for help placing the project section.
The gallery needs category controls before visitors can narrow the visible work. Each button carries the filter value that later behavior will read.
- Place your cursor between the opening and closing tags of the filters container.
- Insert these three filter buttons:
<button
class="filter-button active"
type="button"
data-filter="all"
aria-pressed="true"
>
All
</button>
<button
class="filter-button"
type="button"
data-filter="web"
aria-pressed="false"
>
Web
</button>
<button
class="filter-button"
type="button"
data-filter="design"
aria-pressed="false"
>
Design
</button>
How are the filters described?
- Each data-filter value identifies the category that the button represents.
- The active class marks All as the initial selection.
- The aria-pressed value communicates whether each toggle button is currently selected.
- Save index.html.
- Refresh the Integrated Browser preview.
The project controls are now visible. You should see All, Web, and Design below the Projects heading.
Filter buttons outside the section?
- Confirm that all three buttons sit inside the filters container.
- Check that each button keeps its closing </button> tag.
- Ask for help checking the filter-button nesting.
Each card now needs a category that matches one of the controls. The categories stay limited to web and design.
- Place your cursor between the opening and closing tags of the project-grid container.
- Insert these three project cards:
<article class="project-card" data-category="web">
<p class="project-tag">Web</p>
<h3>Community Events Hub</h3>
<p>A responsive event listing that helps neighbors find local activities.</p>
<a href="#contact">Ask me about it</a>
</article>
<article class="project-card" data-category="design">
<p class="project-tag">Design</p>
<h3>Studio Identity</h3>
<p>A compact visual system for a small creative studio and its launch page.</p>
<a href="#contact">Ask me about it</a>
</article>
<article class="project-card" data-category="web">
<p class="project-tag">Web</p>
<h3>Recipe Notebook</h3>
<p>A calm, readable recipe collection designed for phones and tablets.</p>
<a href="#contact">Ask me about it</a>
</article>
How do the cards connect to filters?
- Every card uses the shared project-card class.
- Each data-category value matches a filter-button category.
- The contact links give every project card the same next action.
- Save index.html.
- Refresh the Integrated Browser preview.
Your gallery now shows three projects. Two cards belong to Web and one belongs to Design.
Project cards not grouped together?
- Confirm that every article sits inside the project-grid container.
- Check that each card has one opening article tag and one closing </article> tag.
- Ask for help checking the project-card structure.
The final page sections give visitors a way to get in touch and show how the portfolio was built.
- Find the closing </main> tag.
- Replace that line with the Contact section and footer below:
<section class="contact" id="contact">
<p class="eyebrow">Contact</p>
<h2>Let's build something useful.</h2>
<p>If you have a project in mind, send me a short message.</p>
<a class="button" href="mailto:your-email@example.com">Email me</a>
</section>
</main>
<footer class="site-footer">
<p>Built with HTML, CSS, and JavaScript.</p>
</footer>
What completes the page?
- The contact identifier gives the header and project-card links a shared destination.
- The mailto: link opens the visitor's email program with the sample contact address.
- The footer closes the page with a short technology credit.
- Save index.html.
- Refresh the Integrated Browser preview.
- Select the Contact navigation link.
The page now moves to the Contact section. You should see the sample email link followed by the footer.
Contact section appearing after the footer?
- Confirm that the Contact section appears before </main>.
- Confirm that the footer appears after </main>.
- Ask for help checking the closing page structure.
✔️ Awesome, I've got everything!
Your index.html now contains the complete portfolio structure.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta
name="description"
content="A personal portfolio featuring selected web and design projects."
>
<title>Alex Morgan | Portfolio</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main-content">Skip to main content</a>
<header class="site-header">
<a class="logo" href="#top" aria-label="Alex Morgan home">AM</a>
<nav aria-label="Primary navigation">
<a href="#about">About</a>
<a href="#projects">Projects</a>
<a href="#contact">Contact</a>
</nav>
</header>
<main id="main-content">
<section class="hero" id="top">
<p class="eyebrow">Front-end developer</p>
<h1>Hi, I'm Alex. I build clear, friendly digital experiences.</h1>
<p class="hero-copy">
I turn thoughtful ideas into responsive websites that are simple to
use and easy to understand.
</p>
<a class="button" href="#projects">See my work</a>
</section>
<section class="about" id="about">
<div>
<p class="eyebrow">About</p>
<h2>Design-minded development</h2>
</div>
<div>
<p>
I enjoy combining clean code with practical design. My focus is on
accessible interfaces, responsive layouts, and useful interactions.
</p>
<ul class="skill-list" aria-label="Skills">
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
<li>Responsive design</li>
</ul>
</div>
</section>
<section id="projects">
<div class="section-heading">
<div>
<p class="eyebrow">Selected work</p>
<h2>Projects</h2>
</div>
<div class="filters" aria-label="Filter projects">
<button
class="filter-button active"
type="button"
data-filter="all"
aria-pressed="true"
>
All
</button>
<button
class="filter-button"
type="button"
data-filter="web"
aria-pressed="false"
>
Web
</button>
<button
class="filter-button"
type="button"
data-filter="design"
aria-pressed="false"
>
Design
</button>
</div>
</div>
<div class="project-grid">
<article class="project-card" data-category="web">
<p class="project-tag">Web</p>
<h3>Community Events Hub</h3>
<p>A responsive event listing that helps neighbors find local activities.</p>
<a href="#contact">Ask me about it</a>
</article>
<article class="project-card" data-category="design">
<p class="project-tag">Design</p>
<h3>Studio Identity</h3>
<p>A compact visual system for a small creative studio and its launch page.</p>
<a href="#contact">Ask me about it</a>
</article>
<article class="project-card" data-category="web">
<p class="project-tag">Web</p>
<h3>Recipe Notebook</h3>
<p>A calm, readable recipe collection designed for phones and tablets.</p>
<a href="#contact">Ask me about it</a>
</article>
</div>
<p id="filter-status" class="sr-only" aria-live="polite"></p>
</section>
<section class="contact" id="contact">
<p class="eyebrow">Contact</p>
<h2>Let's build something useful.</h2>
<p>If you have a project in mind, send me a short message.</p>
<a class="button" href="mailto:your-email@example.com">Email me</a>
</section>
</main>
<footer class="site-footer">
<p>Built with HTML, CSS, and JavaScript.</p>
</footer>
</body>
</html>
Personalize the visible content
The sample copy maps every content slot that your finished portfolio needs. A clear replacement plan keeps your name, role, biography, projects, and contact details consistent.
- Identify every visible place where the sample identity Alex Morgan appears.
- Draft your own role, biography, skills, and project summaries for the checkpoint below.
- Choose either web or design for each planned project card.
- Plan the contact address that replaces your-email@example.com before publication.
Test the page controls
The browser now contains every section and control. Testing the page shows what the current files can actually deliver.
- Keep styles.css unchanged.
- Keep script.js unchanged.
- Save index.html.
- Return to the Integrated Browser preview.
Before you refresh, make a quick prediction about whether the page will look polished with an empty stylesheet.
- Refresh the Integrated Browser preview.
You should see the header, hero, About, Skills, Projects, and Contact sections in one plain stack. The three project cards should appear below the filter buttons.
- Select the About navigation link.
- Select the Projects navigation link.
- Select the Contact navigation link.
Each navigation link moves you to its matching section. This confirms that the section identifiers and link destinations agree.
Before you test the filters, make a quick prediction about whether buttons can hide cards while script.js is empty.
- Select the Web filter button.
- Select the Design filter button.
- Select the All filter button.
The project cards remain unchanged after every click. That shortfall is intentional because script.js contains no filter behavior yet.
You have the complete portfolio content working as one navigable page. Its plain appearance now gives you a clear baseline for the visual work ahead.
Page structure not matching the preview?
- Compare index.html with the full-code tab above.
- Confirm that styles.css and script.js remain empty.
- Ask for help checking the final semantic structure.
Your content structure is complete. Next, you will shape it into a polished desktop portfolio with CSS.
Style the Desktop Experience
Your complete portfolio now appears in the browser as one plain stack. Visitors need clearer visual hierarchy so they can quickly find your introduction, projects, and contact details.
This step uses CSS custom properties to create a reusable visual system. You will test the desktop layout at a wide width. You will also test its assumptions at a narrow width.
In this step, get ready to:
- Create the color palette, typography, focus states, header, hero, and button styles.
- Build the About layout, project filters, and three-column project gallery.
- Compare the finished layout at wide and narrow preview widths.
Establish the visual system
A visual system keeps repeated colors and spacing consistent across the page. The first block also turns the existing header into a sticky navigation bar.
- Switch back to styles.css in the Visual Studio Code editor from earlier.
- Define the palette and global page foundation by pasting this code into styles.css:
:root {
--background: #f7f7fb;
--surface: #ffffff;
--text: #18202a;
--muted: #5f6b7a;
--accent: #6c4ff8;
--accent-dark: #4f35d2;
--border: #dde2ea;
--header-background: rgba(247, 247, 251, 0.95);
--shadow: 0 20px 50px rgba(24, 32, 42, 0.1);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; background: var(--background); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.6; }
a { color: var(--accent-dark); }
a:focus-visible, button:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.skip-link { position: absolute; left: 1rem; top: -4rem; z-index: 10; padding: 0.75rem 1rem; background: var(--text); color: var(--surface); }
.skip-link:focus { top: 1rem; }
.site-header { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; max-width: 1100px; margin: 0 auto; padding: 1.25rem 1.5rem; background: var(--header-background); backdrop-filter: blur(12px); }
.logo { color: var(--text); font-size: 1.2rem; font-weight: 800; text-decoration: none; }
nav { display: flex; gap: 1.25rem; }
nav a { color: var(--text); font-weight: 650; text-decoration: none; }
What does this CSS do?
- The :root custom properties store the shared colors and shadow in one place.
- The global rules set predictable element sizing, smooth anchor scrolling, and the page typography.
- The focus rules give keyboard users a visible outline when they reach a link or button.
- The header rules keep the navigation at the top of the preview while the page scrolls.
- Save styles.css.
- Return to the Integrated Browser preview from earlier.
- Refresh the preview.
You should see a pale background behind a sticky header. Good progress. Your shared palette now controls the page foundation.
Still seeing the plain page?
- Confirm that you pasted the rules into styles.css inside the portfolio-site folder.
- Confirm that index.html still links to styles.css.
- Save the stylesheet before refreshing the Integrated Browser.
- Help me find why my portfolio is not loading the styles from styles.css.
The foundation controls the page as a whole. The next block gives the hero a clear focal point through scale, spacing, and a prominent project link.
- Append the hero and button rules after the nav a rule by pasting this code:
main section, .site-footer { width: min(1100px, calc(100% - 3rem)); margin: 0 auto; }
.hero { display: grid; align-content: center; min-height: calc(100vh - 80px); padding: 7rem 0; }
.eyebrow { margin: 0 0 0.75rem; color: var(--accent-dark); font-size: 0.8rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; }
h1, h2, h3, p { margin-top: 0; }
h1 { max-width: 850px; margin-bottom: 1.25rem; font-size: clamp(2.7rem, 8vw, 6.4rem); line-height: 0.98; letter-spacing: -0.055em; }
h2 { font-size: clamp(2rem, 4vw, 3.4rem); line-height: 1.1; }
.hero-copy { max-width: 650px; margin-bottom: 2rem; color: var(--muted); font-size: 1.15rem; }
.button { justify-self: start; display: inline-block; padding: 0.85rem 1.2rem; border-radius: 999px; background: var(--accent); color: #ffffff; font-weight: 750; text-decoration: none; }
.button:hover { background: var(--accent-dark); }
How does the hero gain hierarchy?
- The shared width rule centers each section inside a maximum width of 1100px.
- The .hero rule gives the introduction enough height and spacing to lead the page.
- The heading sizes use clamp() so the text scales within controlled limits.
- The .button rules turn the project link into a rounded call to action.
- Save styles.css.
- Return to the Integrated Browser preview from earlier.
- Refresh the preview.
You should see a large introduction filling most of the first screen. The purple project link should look like a rounded button.
Hero text missing its scale?
- Check that the heading selector is written as h1.
- Check that the hero selector begins with a full stop as .hero.
- Compare each closing brace with the code block above.
- Help me troubleshoot the hero styles in my portfolio.
Build the desktop layouts
The lower sections need structure that separates supporting information from featured work. Grid and flexible rows create that structure for a wide screen.
- Append the About and filter layout rules after the .button:hover rule by pasting this code:
.about, #projects, .contact { padding: 6rem 0; }
.about { display: grid; grid-template-columns: 0.8fr 1.2fr; gap: 3rem; border-top: 1px solid var(--border); }
.skill-list { display: flex; flex-wrap: wrap; gap: 0.65rem; padding: 0; list-style: none; }
.skill-list li, .project-tag { padding: 0.35rem 0.7rem; border-radius: 999px; background: var(--surface); color: var(--muted); font-size: 0.85rem; font-weight: 700; }
.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 2rem; margin-bottom: 2rem; }
.filters { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.filter-button { padding: 0.65rem 0.9rem; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--text); cursor: pointer; font: inherit; font-weight: 700; }
.filter-button.active { border-color: var(--accent); background: var(--accent); color: #ffffff; }
What changes in these sections?
- The .about grid places its heading beside the biography and skills.
- The skill items use rounded surfaces to make each skill easy to scan.
- The section heading keeps the project title opposite the filter controls.
- The .active rule highlights the selected filter with the accent color.
- Save styles.css.
- Return to the Integrated Browser preview from earlier.
- Refresh the preview.
You should see a two-column About section with pill-shaped skills. The filter buttons should sit beside the Projects heading.
About section still stacked?
- Confirm that the selector is .about.
- Check that grid-template-columns contains both column values.
- Confirm that the Integrated Browser is wide enough to show both columns.
- Help me fix the About and filter layout styles.
The project gallery is the main proof of your work. A three-column grid gives each card equal visual weight across a wide preview.
- Complete the desktop stylesheet by appending this code after the .filter-button.active rule:
.project-grid { display: grid; grid-template-columns: repeat(3, minmax(240px, 1fr)); gap: 1.25rem; }
.project-card { min-height: 280px; padding: 1.5rem; border: 1px solid var(--border); border-radius: 1.25rem; background: var(--surface); box-shadow: var(--shadow); }
.project-card h3 { font-size: 1.4rem; }
.project-card a { font-weight: 750; }
.project-tag { display: inline-block; }
.project-card.hidden { display: none; }
.contact { text-align: center; }
.contact .button { justify-self: auto; }
.site-footer { padding: 2rem 0 3rem; border-top: 1px solid var(--border); color: var(--muted); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
How does the gallery work?
- The .project-grid rule creates three columns with a minimum card width of 240px.
- Each .project-card gets spacing, a border, rounded corners, and a shadow.
- The .hidden class is ready to remove filtered cards from the layout in the next step.
- The .sr-only rule keeps the live filter status available to assistive technology without displaying it visually.
- Save styles.css.
- Return to the Integrated Browser preview from earlier.
- Refresh the preview at a wide width.
You should see three raised project cards in one row. That is the desktop experience complete. Your portfolio now has a clear path from introduction to contact.
Project cards missing their columns?
- Confirm that all three cards still use the project-card class in index.html.
- Check that the gallery container still uses the project-grid class.
- Compare the grid-template-columns value with the code block above.
- Help me troubleshoot the three-column project gallery.
✔️ Awesome, I've got everything!
Great. Double-check that you saved styles.css.
ⓧ I'd like to double check the full code
Compare your complete desktop stylesheet with this version. It should end after the .sr-only rule.
:root {
--background: #f7f7fb;
--surface: #ffffff;
--text: #18202a;
--muted: #5f6b7a;
--accent: #6c4ff8;
--accent-dark: #4f35d2;
--border: #dde2ea;
--header-background: rgba(247, 247, 251, 0.95);
--shadow: 0 20px 50px rgba(24, 32, 42, 0.1);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; background: var(--background); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.6; }
a { color: var(--accent-dark); }
a:focus-visible, button:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.skip-link { position: absolute; left: 1rem; top: -4rem; z-index: 10; padding: 0.75rem 1rem; background: var(--text); color: var(--surface); }
.skip-link:focus { top: 1rem; }
.site-header { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; max-width: 1100px; margin: 0 auto; padding: 1.25rem 1.5rem; background: var(--header-background); backdrop-filter: blur(12px); }
.logo { color: var(--text); font-size: 1.2rem; font-weight: 800; text-decoration: none; }
nav { display: flex; gap: 1.25rem; }
nav a { color: var(--text); font-weight: 650; text-decoration: none; }
main section, .site-footer { width: min(1100px, calc(100% - 3rem)); margin: 0 auto; }
.hero { display: grid; align-content: center; min-height: calc(100vh - 80px); padding: 7rem 0; }
.eyebrow { margin: 0 0 0.75rem; color: var(--accent-dark); font-size: 0.8rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; }
h1, h2, h3, p { margin-top: 0; }
h1 { max-width: 850px; margin-bottom: 1.25rem; font-size: clamp(2.7rem, 8vw, 6.4rem); line-height: 0.98; letter-spacing: -0.055em; }
h2 { font-size: clamp(2rem, 4vw, 3.4rem); line-height: 1.1; }
.hero-copy { max-width: 650px; margin-bottom: 2rem; color: var(--muted); font-size: 1.15rem; }
.button { justify-self: start; display: inline-block; padding: 0.85rem 1.2rem; border-radius: 999px; background: var(--accent); color: #ffffff; font-weight: 750; text-decoration: none; }
.button:hover { background: var(--accent-dark); }
.about, #projects, .contact { padding: 6rem 0; }
.about { display: grid; grid-template-columns: 0.8fr 1.2fr; gap: 3rem; border-top: 1px solid var(--border); }
.skill-list { display: flex; flex-wrap: wrap; gap: 0.65rem; padding: 0; list-style: none; }
.skill-list li, .project-tag { padding: 0.35rem 0.7rem; border-radius: 999px; background: var(--surface); color: var(--muted); font-size: 0.85rem; font-weight: 700; }
.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 2rem; margin-bottom: 2rem; }
.filters { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.filter-button { padding: 0.65rem 0.9rem; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--text); cursor: pointer; font: inherit; font-weight: 700; }
.filter-button.active { border-color: var(--accent); background: var(--accent); color: #ffffff; }
.project-grid { display: grid; grid-template-columns: repeat(3, minmax(240px, 1fr)); gap: 1.25rem; }
.project-card { min-height: 280px; padding: 1.5rem; border: 1px solid var(--border); border-radius: 1.25rem; background: var(--surface); box-shadow: var(--shadow); }
.project-card h3 { font-size: 1.4rem; }
.project-card a { font-weight: 750; }
.project-tag { display: inline-block; }
.project-card.hidden { display: none; }
.contact { text-align: center; }
.contact .button { justify-self: auto; }
.site-footer { padding: 2rem 0 3rem; border-top: 1px solid var(--border); color: var(--muted); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
Test the desktop assumption
A desktop layout can look convincing while there is plenty of horizontal space. Testing the same rules in a narrow preview reveals whether that assumption still holds.
- Widen the Integrated Browser preview until all three project cards fit comfortably in one row.
- Refresh the preview.
You should see the polished desktop layout with a two-column About section. You should also see three project cards across the gallery.
Before you narrow the preview, predict whether three cards with a minimum width of 240px will continue to fit.
- Drag the edge of the Integrated Browser until the preview is narrower than 760px.
You should see crowded cards with horizontal overflow. This shortfall is intentional. The desktop grid has reached the limit of its current layout rules.
Before you test the filters, predict whether styling alone has added their behavior.
- Click the Web filter.
- Click the Design filter.
All three cards should remain visible. The filters are still inert because script.js remains empty.
You now have a polished desktop portfolio plus clear evidence of its two remaining gaps. Next, you will repair the narrow layout and make every project filter work.
Make It Responsive and Interactive
Your desktop portfolio now has clear visual hierarchy. The narrow preview exposed a crowded grid with horizontal overflow.
This step uses CSS to adapt the layout below 760 pixels. It also uses JavaScript to turn the filter buttons into working controls.
In this step, get ready to:
- Stack the portfolio into a readable narrow-screen layout.
- Connect the project filters to the matching cards.
- Verify that responsiveness and filtering work together.
Repair the narrow layout
A media query applies a group of CSS rules when the viewport meets a condition. This project switches layouts when the preview is 760 pixels wide or narrower.
- In the Visual Studio Code editor from earlier, switch back to styles.css.
- Scroll to the end of styles.css.
- Add the narrow-screen layout by pasting this code:
@media (max-width: 760px) {
.site-header,
.section-heading {
align-items: flex-start;
flex-direction: column;
}
nav {
flex-wrap: wrap;
}
.hero {
min-height: auto;
padding: 5rem 0;
}
.about {
grid-template-columns: 1fr;
gap: 1rem;
}
.project-grid {
grid-template-columns: 1fr;
}
}
What does this code do?
- The @media (max-width: 760px) condition activates the nested rules at narrow widths.
- The .site-header and .section-heading layouts switch to vertical columns.
- The nav links wrap when their row runs out of space.
- The .hero uses less vertical space on a narrow screen.
- The .about and .project-grid layouts switch to one column.
- Save styles.css.
Before you refresh, do you think any portfolio section can still force the page beyond the narrow viewport?
- Keep the Integrated Browser below 760 pixels wide.
- Refresh the Integrated Browser preview.
You will see the header content stacked vertically. The About section and project cards now form single-column layouts without horizontal scrolling.
Still seeing horizontal overflow?
- Check that the media query sits after the existing desktop rules in styles.css.
- Confirm that the breakpoint says max-width: 760px.
- Check that the project selector is .project-grid.
- Help me find why my portfolio still overflows below 760 pixels.
✔️ Awesome, I've got everything!
Great. Your narrow-screen CSS is saved and the portfolio now fits inside the preview.
ⓧ I'd like to double check the full code
Compare your complete styles.css file with this reference.
:root { --background: #f7f7fb; --surface: #ffffff; --text: #18202a; --muted: #5f6b7a; --accent: #6c4ff8; --accent-dark: #4f35d2; --border: #dde2ea; --header-background: rgba(247, 247, 251, 0.95); --shadow: 0 20px 50px rgba(24, 32, 42, 0.1); }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; background: var(--background); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.6; }
a { color: var(--accent-dark); }
a:focus-visible, button:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.skip-link { position: absolute; left: 1rem; top: -4rem; z-index: 10; padding: 0.75rem 1rem; background: var(--text); color: var(--surface); }
.skip-link:focus { top: 1rem; }
.site-header { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; max-width: 1100px; margin: 0 auto; padding: 1.25rem 1.5rem; background: var(--header-background); backdrop-filter: blur(12px); }
.logo { color: var(--text); font-size: 1.2rem; font-weight: 800; text-decoration: none; }
nav { display: flex; gap: 1.25rem; }
nav a { color: var(--text); font-weight: 650; text-decoration: none; }
main section, .site-footer { width: min(1100px, calc(100% - 3rem)); margin: 0 auto; }
.hero { display: grid; align-content: center; min-height: calc(100vh - 80px); padding: 7rem 0; }
.eyebrow { margin: 0 0 0.75rem; color: var(--accent-dark); font-size: 0.8rem; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; }
h1, h2, h3, p { margin-top: 0; }
h1 { max-width: 850px; margin-bottom: 1.25rem; font-size: clamp(2.7rem, 8vw, 6.4rem); line-height: 0.98; letter-spacing: -0.055em; }
h2 { font-size: clamp(2rem, 4vw, 3.4rem); line-height: 1.1; }
.hero-copy { max-width: 650px; margin-bottom: 2rem; color: var(--muted); font-size: 1.15rem; }
.button { justify-self: start; display: inline-block; padding: 0.85rem 1.2rem; border-radius: 999px; background: var(--accent); color: #ffffff; font-weight: 750; text-decoration: none; }
.button:hover { background: var(--accent-dark); }
.about, #projects, .contact { padding: 6rem 0; }
.about { display: grid; grid-template-columns: 0.8fr 1.2fr; gap: 3rem; border-top: 1px solid var(--border); }
.skill-list { display: flex; flex-wrap: wrap; gap: 0.65rem; padding: 0; list-style: none; }
.skill-list li, .project-tag { padding: 0.35rem 0.7rem; border-radius: 999px; background: var(--surface); color: var(--muted); font-size: 0.85rem; font-weight: 700; }
.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 2rem; margin-bottom: 2rem; }
.filters { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.filter-button { padding: 0.65rem 0.9rem; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--text); cursor: pointer; font: inherit; font-weight: 700; }
.filter-button.active { border-color: var(--accent); background: var(--accent); color: #ffffff; }
.project-grid { display: grid; grid-template-columns: repeat(3, minmax(240px, 1fr)); gap: 1.25rem; }
.project-card { min-height: 280px; padding: 1.5rem; border: 1px solid var(--border); border-radius: 1.25rem; background: var(--surface); box-shadow: var(--shadow); }
.project-card h3 { font-size: 1.4rem; }
.project-card a { font-weight: 750; }
.project-tag { display: inline-block; }
.project-card.hidden { display: none; }
.contact { text-align: center; }
.contact .button { justify-self: auto; }
.site-footer { padding: 2rem 0 3rem; border-top: 1px solid var(--border); color: var(--muted); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
@media (max-width: 760px) { .site-header, .section-heading { align-items: flex-start; flex-direction: column; } nav { flex-wrap: wrap; } .hero { min-height: auto; padding: 5rem 0; } .about { grid-template-columns: 1fr; gap: 1rem; } .project-grid { grid-template-columns: 1fr; } }
How to use this reference
This reference shows the complete stylesheet at this point. Compare selectors and values without replacing any personal content in index.html.
Wire the filter controls
The filter buttons need DOM events so each click can update the visible cards. The script also updates the pressed state and the screen-reader status message.
- Select script.js in the Visual Studio Code Explorer sidebar.
- Give the filter logic access to the page elements by pasting these lines at the top of script.js:
const filterButtons = document.querySelectorAll(".filter-button");
const projectCards = document.querySelectorAll(".project-card");
const filterStatus = document.querySelector("#filter-status");
What do these references hold?
- The filterButtons reference holds every element with the .filter-button class.
- The projectCards reference holds every element with the .project-card class.
- The filterStatus reference points to the hidden live-status element.
- Add the click behavior directly below those references by pasting this code:
filterButtons.forEach((button) => {
button.addEventListener("click", () => {
const selectedFilter = button.dataset.filter;
filterButtons.forEach((item) => {
const isActive = item === button;
item.classList.toggle("active", isActive);
item.setAttribute("aria-pressed", String(isActive));
});
let visibleCount = 0;
projectCards.forEach((card) => {
const shouldShow =
selectedFilter === "all" || card.dataset.category === selectedFilter;
card.classList.toggle("hidden", !shouldShow);
if (shouldShow) {
visibleCount += 1;
}
});
filterStatus.textContent = `${visibleCount} project${
visibleCount === 1 ? "" : "s"
} shown`;
});
});
How does the filter work?
- Each filter button receives a click listener.
- The selectedFilter value comes from the clicked button's data-filter attribute.
- The isActive value controls the active class and aria-pressed state.
- The shouldShow value compares each card category with the selected filter.
- The visibleCount value produces the live status text for assistive technology.
- Save script.js.
Before you test the buttons, which cards do you expect the Web filter to keep visible?
- Refresh the Integrated Browser preview.
- Click Web.
- Click Design.
- Click All.
The Web filter shows two project cards. The Design filter shows one project card.
The All filter restores all three cards. Each clicked button receives the active styling.
Filters still doing nothing?
- Confirm that script.js is saved.
- Check that the three element references appear before the click-listener code.
- Compare .filter-button and .project-card with the class names in index.html.
- Help me debug project filters that do not react when clicked.
✔️ Awesome, I've got everything!
Your filter buttons now change the visible project cards and active state.
ⓧ I'd like to double check the full code
Compare your complete script.js file with this reference.
const filterButtons = document.querySelectorAll(".filter-button");
const projectCards = document.querySelectorAll(".project-card");
const filterStatus = document.querySelector("#filter-status");
filterButtons.forEach((button) => {
button.addEventListener("click", () => {
const selectedFilter = button.dataset.filter;
filterButtons.forEach((item) => {
const isActive = item === button;
item.classList.toggle("active", isActive);
item.setAttribute("aria-pressed", String(isActive));
});
let visibleCount = 0;
projectCards.forEach((card) => {
const shouldShow =
selectedFilter === "all" || card.dataset.category === selectedFilter;
card.classList.toggle("hidden", !shouldShow);
if (shouldShow) {
visibleCount += 1;
}
});
filterStatus.textContent = `${visibleCount} project${
visibleCount === 1 ? "" : "s"
} shown`;
});
});
How to use this reference
This reference shows the complete filter script. Match every selector and project-internal identifier exactly.
Test both behaviors together
Responsive layout rules and filter logic affect the same project gallery. The final check proves that both behaviors survive a viewport change.
Before the final check, do you think the active filter will keep working when the layout changes from one column to three?
- Keep the Integrated Browser below 760 pixels wide.
- Refresh the portfolio preview.
- Click Web.
- Click Design.
- Click All.
You will see one project column with no horizontal scrollbar. Each filter shows only its matching cards.
- Widen the Integrated Browser beyond 760 pixels.
- Click Web.
You will see the three-column desktop layout return. The Web filter continues to show only the two web project cards.
That is the core experience working: your portfolio now adapts to narrow screens and responds to project filters. Next, you will publish it through GitHub Pages.
Publish with GitHub Pages
Your portfolio now adapts cleanly to desktop and mobile widths. It still lives only in your local preview, so nobody else can open it.
GitHub Pages publishes HTML, CSS, and JavaScript files from a repository as a public website. That public address turns your local project into something anyone can visit.
You will place your portfolio files in a public GitHub repository. You will publish the final files from its main branch.
In this step, get ready to:
- Create a public repository with a README.
- Merge your portfolio files into main through a pull request.
- Publish the repository with GitHub Pages and test the public site.
Create the public repository
A public repository gives GitHub Pages access to the files that make up your site. GitHub Free includes this publishing path for public repositories.
Why does the repository stay public?
Everything uploaded to this repository is visible to visitors. That visibility is intentional because these files power your public portfolio.
- In a new browser tab, sign in to GitHub with your existing account.
- On the GitHub home page, select New repository.
- Enter portfolio-site as the repository name.
- Choose the public visibility option.
- Turn the Add README toggle to On.
- Click Create repository.
Your first public piece is in place: the portfolio-site repository now has a main branch with README.md ready for your site files.
Upload and merge the portfolio files
A pull request gives you a review point before new files reach main. This workflow keeps the published branch deliberate.
- On the repository page, click Add file.
- Select Upload files.
- Click choose your files.
- Select index.html, styles.css, and script.js from your local portfolio-site folder.
- Choose the new branch option for this upload.
- Click Propose changes.
- Click Compare & pull request.
- Click Create Pull Request.
You will see a pull request containing the three portfolio files. It is ready to move into the publishing branch.
- Merge the upload by clicking Merge pull request.
- Complete the merge by clicking Confirm merge.
- Return to the repository by selecting portfolio-site in the page breadcrumb.
That merge puts your finished site on main. You should see README.md, index.html, styles.css, and script.js in the repository file list.
Missing a portfolio file?
- Confirm the branch selector shows main.
- Check that the pull request shows a merged state.
- Repeat the upload for any missing file.
- Help me check why my uploaded portfolio files are missing from the main branch.
Enable branch publishing
Branch publishing tells GitHub Pages exactly where your finished website files live. The repository root already contains index.html, so it can serve as the publishing folder.
- In the repository navigation, click Settings.
- In the settings sidebar, select Pages.
- Under Source, choose Deploy from a branch.
- Select main as the publishing branch.
- Select / (root) as the publishing folder.
- Click Save.
Why branch publishing?
Your portfolio uses plain static files with no custom build process. Branch publishing sends those files directly to the website without adding a separate workflow.
The first publication can take up to 10 minutes, so a short wait here is expected. GitHub is creating the public deployment behind the scenes.
- Refresh the Pages settings until Visit site appears under the GitHub Pages status.
Before you open the public version, do you expect its layout and filters to behave exactly like your local preview?
- Open the published portfolio by clicking Visit site.
You should see your complete portfolio at its public GitHub Pages address. The header, hero, About, Skills, Projects, and Contact sections should match your local preview.
- Record the live address here: https://<owner>.github.io/portfolio-site.
- Select the Web project filter.
You should see only the two web project cards. The Web button should show the active state.
- Select the Design project filter.
You should see only the Studio Identity card. The Design button should show the active state.
- Select the All project filter.
- Narrow the browser window below 760px.
You should see all three cards in one column with no horizontal scrollbar. Widening the browser restores the three-column layout.
Your portfolio is now public, responsive, and interactive at a shareable address.
Site not ready or missing styles?
- Allow up to 10 minutes for the first publication.
- Confirm Pages uses Deploy from a branch with branch main and folder / (root).
- Confirm index.html, styles.css, and script.js appear at the top level of main.
- Help me troubleshoot why my GitHub Pages portfolio is not publishing correctly.
✔️ Awesome, I've got everything!
Great work. Your repository contains the final site files on main, and the public deployment works at desktop and mobile widths.
ⓧ I'd like to double check the full code
Your repository and GitHub Pages settings should match this final state:
- The public repository is named portfolio-site.
- The merged main branch contains README.md, index.html, styles.css, and script.js.
- The Pages source is Deploy from a branch.
- The publishing branch is main.
- The publishing folder is / (root).
- The public site opens at https://<owner>.github.io/portfolio-site.
Secret mission
Add a Persistent Theme Switch
Give visitors control over your portfolio's color scheme with a light and dark theme switch. Save their choice so the selected theme returns after every reload on your published site.
Clean Up Your Resources
Clean Up Your Resources
Your public site uses GitHub Pages, which is included for public repositories on GitHub Free. Choose whether to keep the site public, unpublish it for now, or delete the repository permanently.
Resources you used:
- Public GitHub repository named portfolio-site.
- GitHub Pages deployment at https://<owner>.github.io/portfolio-site.
Keep everything running
No action is needed. Choose this option if you want your portfolio to remain public.
- Leave the portfolio-site repository public.
- Keep Source set to Deploy from a branch.
- Keep main selected as the publishing branch.
- Keep / (root) selected as the publishing folder.
Your portfolio remains available at https://<owner>.github.io/portfolio-site.
Pause - I'll come back to this later
Unpublish the site if you want to take it offline temporarily. Your repository files remain intact.
- Return to the Settings page in the portfolio-site repository.
- Select Pages in the sidebar.
- Open the menu beside the live-site message in the GitHub Pages area.
- Select Unpublish site.
GitHub removes the current deployment. Your repository content and settings stay unchanged.
Delete - I don't want to use this again
Deleting the repository permanently removes its Pages site too. Choose this option only when you no longer need either resource.
- Return to the Settings page in the portfolio-site repository.
- Select Pages in the sidebar.
- Open the menu beside the live-site message in the GitHub Pages area.
- Select Unpublish site.
- Select General in the sidebar.
- Scroll to Danger Zone.
- Click Delete this repository.
- Complete GitHub's confirmation steps.
Your repository and its Pages site are permanently removed.
Nice Work!
Nice Work!
You did it! Your responsive personal portfolio is live at a shareable URL. Visitors can explore your work through a polished experience that adapts to their screen.
You've learned how to:
- Used Semantic HTML to build a one-page portfolio with clear navigation between your introduction, projects, and contact details.
- Repaired narrow-screen overflow with a responsive CSS media query. Wired accessible project filters through JavaScript DOM events.
- Published the finished site from a public GitHub repository through GitHub Pages. Your portfolio now has a public URL you can share.
- Completed the Secret Mission by adding a persistent light and dark theme switch. The site uses localStorage to remember each visitor's choice after reload.
Ready to quiz yourself?