Build and Deploy a Portfolio Website
Build a responsive portfolio site with dark mode and deploy it live.
Introduction
30 Second Summary
Every developer needs a home on the internet. Whether you're applying for internships, showcasing class projects, or building your personal brand, a portfolio website is the single best way to prove you can actually build things.
In this project, you will build a responsive, animated portfolio website from scratch and deploy it live to the internet using GitHub Pages.
What You'll Build
A multi-section portfolio website featuring responsive design, dark/light mode, typing animations, scroll-reveal effects, and live deployment with Git.
By the end of this project, you'll have:
- A responsive portfolio website with a hero section, projects grid using CSS Grid, and a contact section.
- A dark/light mode toggle that remembers visitor preferences using localStorage.
- A live website deployed to GitHub Pages with your own public URL.
- Secret Mission: Add a filterable projects section where visitors can filter your work by category.
Are there any prerequisites?
You should have basic familiarity with HTML, CSS, and JavaScript. You also need a GitHub account and VS Code installed on your computer.
Before We Start
Before We Start...
Install and Configure Git
To get your portfolio website live on the internet, you need a way to send your code to GitHub. That's where Git comes in. Git is a version control tool that tracks changes to your files and lets you push them to a remote repository.
In this step, you'll install Git, configure it with your identity, and create the GitHub repository that will host your live site on GitHub Pages.
In this step, get ready to:
- Install and verify Git on your machine.
- Configure Git with your name and email.
- Create your GitHub Pages repository.
Install Git
Git is a command-line tool, so you'll install and use it from PowerShell (the terminal built into Windows).
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Check whether Git is already installed by running this command:
git --version
Look at what PowerShell shows you and pick the matching tab below.
✔️ I see version 2.28 or higher
If you see something like git version 2.54.0.windows.1 (or any version number at or above 2.28), Git is ready to go.
Skip ahead to the "Configure Git identity" substep below.
ⓧ I see a version lower than 2.28
Your Git installation is outdated and needs to be upgraded. This project requires Git 2.28+ for the git branch -M command to work.
- Upgrade Git to the latest version by running this command:
winget upgrade --id Git.Git -e --source winget
- Close PowerShell completely after the upgrade finishes.
- Reopen PowerShell (press the Windows key, type PowerShell, press Enter).
- Verify the upgrade by running:
git --version
You should now see git version 2.54.0.windows.1 or similar.
ⓧ Command not found
Git is not installed yet. You'll install it using winget, the Windows package manager.
- Install Git by running this command:
winget install --id Git.Git -e --source winget
What does this command do?
This tells winget to install Git from the official Windows package source. The -e flag ensures an exact match so you get the right package.
- Wait for the installation to complete. You may see a progress bar and prompts to accept license terms.
- Close PowerShell completely after the installation finishes.
- Reopen PowerShell (press the Windows key, type PowerShell, press Enter).
- Verify Git installed correctly by running:
git --version
You should see git version 2.54.0.windows.1 or similar.
Still seeing 'command not found'?
Closing and reopening PowerShell is essential. The new install adds Git to your system PATH, but PowerShell only reads PATH when it first opens.
If it still doesn't work after reopening, try restarting your computer. This forces Windows to reload all PATH changes.
Help me troubleshoot my Git installation on Windows.
Configure Git identity
Before you can push code to GitHub, Git needs to know who you are. Every commit you make gets stamped with your name and email address.
- Set your Git username by running this command (replace the placeholder with your real name):
git config --global user.name "[[YOUR_NAME="Your Name"]]"
- Set your Git email by running this command (use the same email tied to your GitHub account):
git config --global user.email "[[YOUR_EMAIL="your-email@example.com"]]"
Why must the email match my GitHub account?
GitHub links commits to your profile using the email address in your Git config. If the email doesn't match, your commits will show up as authored by an unknown user instead of showing your profile picture and name.
- Confirm your settings saved correctly by running:
git config --global --list
You should see your user.name and user.email listed in the output.
Don't see your name or email?
Make sure you included the double quotes around your name and email in the commands. Without quotes, Git may only save the first word.
Help me fix my Git configuration.
Create your GitHub repository
GitHub Pages hosts your website directly from a GitHub repository. For a personal site, the repository must follow a specific naming pattern.
- Open Google Chrome and go to github.com.
- Sign in to your GitHub account.
- Click the + icon in the top-right corner of the page.
- Select New repository.
- In the Repository name field, enter yourusername.github.io (replace yourusername with your actual GitHub username).
- Make sure Public is selected.
- Leave the Add a README file checkbox unchecked.
- Click Create repository.
Why this exact name?
GitHub Pages gives every user one free website at https://yourusername.github.io. For this to work, the repository must be named exactly yourusername.github.io using your real GitHub username.
We skip the README so the repository stays empty. You'll push your own files to it in a later step.
After clicking Create repository, GitHub will show you an empty repository page with setup instructions. Leave this page open. You'll come back to it in Step 5.
Seeing 'repository already exists'?
If you've previously created a repository with this name, you'll need to delete it first. Go to the existing repository's Settings tab, scroll to the bottom, and click Delete this repository. Then try creating it again.
Help me fix the 'repository already exists' error on GitHub.
Your tools are locked and loaded. Next up, you'll build the HTML structure that gives your portfolio its shape and content.
Build the HTML Structure
Your development environment is ready. Git is configured, your GitHub repository is waiting, and VS Code is open.
Now it's time to build the actual portfolio page. In this step, you'll create the project files and write the full HTML structure that holds your name, your projects, and your contact information.
In this step, get ready to:
- Create your project folder and files in VS Code.
- Write the full HTML structure with semantic elements.
- Link your CSS and JavaScript files and preview the page in Chrome.
Create your project folder and files
Every website starts with a folder to hold its files. You'll create one on your Desktop so it's easy to find later.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop in the file browser that appears.
- Click New Folder (or right-click and select New Folder).
- Name the folder portfolio and select it.
- Click Select Folder (Windows) or Open (macOS) to open it in VS Code.
VS Code's Explorer sidebar on the left now shows your empty portfolio folder.
- Hover over the PORTFOLIO heading in the Explorer sidebar to reveal the action icons.
- Click the New File icon (a page with a plus sign).
- Type index.html and press Enter.
- Repeat to create a second file named style.css.
- Repeat once more to create a third file named script.js.
- Confirm the files exist by checking the Explorer sidebar.
You should see all three files listed: index.html, style.css, and script.js.
Don't see the New File icon?
Make sure you opened a folder, not just a file. If the Explorer sidebar says No Folder Opened, go back to File and select Open Folder to open your portfolio folder.
Help me find the New File icon in VS Code
Write the HTML structure
Your portfolio page needs a clear structure that browsers and screen readers can understand. HTML5 provides semantic elements like <header>, <main>, <section>, and <footer> that describe the purpose of each part of your page.
- Click on index.html in the Explorer sidebar to open it in the editor.
- Copy the following code into index.html. This creates the document skeleton, navigation bar, and hero section:
<!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>
</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="#projects">Projects</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode">🌙</button>
</nav>
</header>
<main>
<section class="hero" id="hero">
<h1>Hi, I'm <span class="highlight">Your Name</span></h1>
<p class="subtitle" id="typing-text"></p>
<a href="#projects" class="cta-button">View My Work</a>
</section>
What does this code do?
The <nav class="navbar"> creates a navigation bar with three links (About, Projects, Contact) and a button for dark mode toggling later.
The <section class="hero"> is the first thing visitors see. It shows your name, a subtitle paragraph where the typing animation will appear, and a call-to-action button.
The class="reveal" you'll see on later sections is a hook for scroll animations. JavaScript will add an active class when the section scrolls into view.
You won't see a visible page yet. The next chunk adds the remaining content sections.
- Directly below the hero section code you just pasted (after the closing </section> of the hero), add the about, projects, contact sections, footer, and closing tags:
<section class="about reveal" id="about">
<h2>About Me</h2>
<p>I'm a first-year Software Engineering student passionate about building things for the web. I love turning ideas into reality through code and am always eager to learn new technologies.</p>
</section>
<section class="projects reveal" id="projects">
<h2>My Projects</h2>
<div class="projects-grid">
<div class="project-card">
<h3>Project One</h3>
<p>A brief description of your first project. What problem does it solve? What did you learn?</p>
<div class="project-tags">
<span class="tag">HTML</span>
<span class="tag">CSS</span>
</div>
</div>
<div class="project-card">
<h3>Project Two</h3>
<p>A brief description of your second project. What technologies did you use?</p>
<div class="project-tags">
<span class="tag">JavaScript</span>
<span class="tag">API</span>
</div>
</div>
<div class="project-card">
<h3>Project Three</h3>
<p>A brief description of your third project. What makes it interesting?</p>
<div class="project-tags">
<span class="tag">Python</span>
<span class="tag">Data</span>
</div>
</div>
</div>
</section>
<section class="contact reveal" id="contact">
<h2>Get In Touch</h2>
<p>I'm always open to new opportunities and conversations.</p>
<div class="contact-links">
<a href="https://github.com/yourusername" class="contact-link">GitHub</a>
<a href="https://linkedin.com/in/yourusername" class="contact-link">LinkedIn</a>
<a href="mailto:your-email@example.com" class="contact-link">Email</a>
</div>
</section>
</main>
<footer>
<p>© 2026 Your Name. Built from scratch.</p>
</footer>
</body>
</html>
What does this code do?
Each <section> represents a distinct part of your portfolio. The about section introduces you. The projects section uses a projects-grid div that will become a responsive CSS Grid layout later.
Each project-card div holds a title, description, and tags. The contact section provides links to your GitHub, LinkedIn, and email.
Notice that the about, projects, and contact sections all have class="reveal". This is the hook that JavaScript will use to fade them in on scroll.
- Save index.html by pressing Ctrl+S (Windows) or Cmd+S (macOS).
- Double-click index.html in your file explorer (Finder on macOS or File Explorer on Windows) to open it in Chrome.
You should see all your text content displayed in plain, unstyled format. The navigation links, your name, project descriptions, and contact links are all visible as raw text.
Seeing a blank page?
Check that you pasted both code chunks into the same file. The second chunk goes directly below the first, not in a separate file.
Also make sure there are no extra or missing closing tags. The </main> tag should appear after the contact section closes, and </body> comes after the footer.
Help me debug my blank HTML page
Link your CSS and JS files
Your HTML page works on its own, but it doesn't know about the stylesheet or JavaScript file yet. Two link tags connect everything together.
- In index.html, find this line inside the <head> section:
<title>Your Name - Portfolio</title>
- Add the stylesheet link on the next line, so your <head> now looks like this:
<title>Your Name - Portfolio</title>
<link rel="stylesheet" href="style.css">
- Next, find the </footer> closing tag near the bottom of the file.
- Add the script tag between </footer> and </body>, so the end of your file looks like this:
<footer>
<p>© 2026 Your Name. Built from scratch.</p>
</footer>
<script src="script.js"></script>
</body>
</html>
Why link CSS in the head and JS before the closing body tag?
The <link> tag in the <head> tells the browser to load your stylesheet before rendering the page. This prevents a flash of unstyled content.
The <script> tag before </body> ensures the HTML is fully loaded before JavaScript tries to interact with it. If the script ran earlier, it might try to find elements that don't exist yet.
- Save index.html.
Now let's set up a live preview so you can see changes instantly as you code.
- In VS Code, click the Extensions icon in the left sidebar (it looks like four squares).
- Type Live Server in the search bar.
- Find the extension by Ritwick Dey and click Install.
- Once installed, right-click on index.html in the Explorer sidebar.
- Click Open with Live Server.
Chrome opens automatically with your portfolio page. You should see all your text content (navigation links, your name, project cards, contact links) displayed in the browser's default styling.
What is Live Server?
Live Server is a VS Code extension that runs a local web server and automatically refreshes your browser every time you save a file. This means you can see your CSS and JavaScript changes instantly without manually refreshing.
Live Server not opening?
Make sure you right-clicked on index.html specifically, not on style.css or script.js. Live Server needs an HTML file to serve.
If you don't see the Open with Live Server option, try closing and reopening VS Code after installing the extension.
Help me get Live Server working
✔️ Awesome, I've got everything!
Great. Make sure index.html is saved and Live Server is running in the background.
ⓧ I'd like to double check the full code
Here is the complete index.html file. Make sure your code matches this exactly:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Name - Portfolio</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<nav class="navbar">
<a href="#" class="logo">YourName</a>
<ul class="nav-links">
<li><a href="#about">About</a></li>
<li><a href="#projects">Projects</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode">🌙</button>
</nav>
</header>
<main>
<section class="hero" id="hero">
<h1>Hi, I'm <span class="highlight">Your Name</span></h1>
<p class="subtitle" id="typing-text"></p>
<a href="#projects" class="cta-button">View My Work</a>
</section>
<section class="about reveal" id="about">
<h2>About Me</h2>
<p>I'm a first-year Software Engineering student passionate about building things for the web. I love turning ideas into reality through code and am always eager to learn new technologies.</p>
</section>
<section class="projects reveal" id="projects">
<h2>My Projects</h2>
<div class="projects-grid">
<div class="project-card">
<h3>Project One</h3>
<p>A brief description of your first project. What problem does it solve? What did you learn?</p>
<div class="project-tags">
<span class="tag">HTML</span>
<span class="tag">CSS</span>
</div>
</div>
<div class="project-card">
<h3>Project Two</h3>
<p>A brief description of your second project. What technologies did you use?</p>
<div class="project-tags">
<span class="tag">JavaScript</span>
<span class="tag">API</span>
</div>
</div>
<div class="project-card">
<h3>Project Three</h3>
<p>A brief description of your third project. What makes it interesting?</p>
<div class="project-tags">
<span class="tag">Python</span>
<span class="tag">Data</span>
</div>
</div>
</div>
</section>
<section class="contact reveal" id="contact">
<h2>Get In Touch</h2>
<p>I'm always open to new opportunities and conversations.</p>
<div class="contact-links">
<a href="https://github.com/yourusername" class="contact-link">GitHub</a>
<a href="https://linkedin.com/in/yourusername" class="contact-link">LinkedIn</a>
<a href="mailto:your-email@example.com" class="contact-link">Email</a>
</div>
</section>
</main>
<footer>
<p>© 2026 Your Name. Built from scratch.</p>
</footer>
<script src="script.js"></script>
</body>
</html>
Your HTML skeleton is complete. Next up, you'll bring it to life with responsive CSS styling, custom properties for theming, and hover animations on your project cards.
Style with Responsive CSS
Your HTML structure is complete, but right now it looks like a plain document with default browser styling.
In this step, you'll transform that raw HTML into a polished, professional design using CSS custom properties, Flexbox, Grid, and responsive breakpoints.
In this step, get ready to:
- Set up a theming system with CSS custom properties and dark mode variables.
- Style the layout using Flexbox for navigation and Grid for the project cards.
- Add responsive breakpoints and scroll-reveal animation classes.
Set up CSS custom properties and base styles
CSS custom properties (also called CSS variables) let you define colors, fonts, and spacing in one place. When you want to change a color across the entire site, you update a single variable instead of hunting through hundreds of lines.
- In VS Code, open style.css and add the following custom properties and dark mode overrides:
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #1a1a2e;
--text-secondary: #4a4a6a;
--accent: #6c63ff;
--accent-hover: #5a52d5;
--card-bg: #ffffff;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--card-shadow-hover: 0 8px 25px rgba(108, 99, 255, 0.2);
--border: #e0e0e0;
--nav-bg: rgba(255, 255, 255, 0.95);
--font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
color-scheme: light dark;
}
[data-theme="dark"] {
--bg-primary: #0f0f1a;
--bg-secondary: #1a1a2e;
--text-primary: #e0e0e0;
--text-secondary: #a0a0b0;
--accent: #7c73ff;
--accent-hover: #9b94ff;
--card-bg: #1a1a2e;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
--card-shadow-hover: 0 8px 25px rgba(124, 115, 255, 0.3);
--border: #2a2a3e;
--nav-bg: rgba(15, 15, 26, 0.95);
}
What does this code do?
The :root selector defines variables that apply to the entire page. Every color, shadow, and font is stored in a named variable like --accent.
The [data-theme="dark"] selector overrides those same variables with darker colors. When JavaScript adds this attribute later, the entire site switches to dark mode instantly because every element references these variables.
The color-scheme: light dark declaration tells the browser this page supports both themes, allowing form controls and scrollbars to adapt automatically.
What are CSS custom properties?
CSS custom properties are variables that start with -- and are referenced with var(). They make your stylesheet maintainable because changing one value updates every element that uses it.
- You won't see a visible change yet. The next chunk uses these variables to style the page.
- Below the dark mode block, add the CSS reset and base styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-main);
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition: background-color 0.3s ease, color 0.3s ease;
}
What does this code do?
The * { margin: 0; padding: 0; box-sizing: border-box; } reset removes default browser spacing so every element starts from a clean slate.
The scroll-behavior: smooth makes anchor links scroll smoothly instead of jumping. The body uses the custom properties you just defined, and the transition ensures background and text color animate smoothly when dark mode toggles.
- Save style.css and check your browser.
- You should see the default browser margins disappear and the font change to a clean system font.
Page looks the same as before?
Make sure you saved style.css. Check that the <link rel="stylesheet" href="style.css"> line in your index.html matches the filename exactly (case-sensitive).
help me debug why my CSS file isn't loading
Style the layout with Flexbox and Grid
Now you'll use Flexbox and CSS Grid to arrange elements on the page. Flexbox handles one-dimensional layouts (rows or columns) while Grid handles two-dimensional layouts (rows AND columns at once).
- Below your base styles in style.css, add the navigation styles:
/* Navigation */
.navbar {
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: var(--nav-bg);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
z-index: 1000;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--accent);
text-decoration: none;
}
.nav-links {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-links a {
text-decoration: none;
color: var(--text-primary);
font-weight: 500;
transition: color 0.3s ease;
}
.nav-links a:hover {
color: var(--accent);
}
.theme-toggle {
background: none;
border: 2px solid var(--border);
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 1.2rem;
cursor: pointer;
transition: border-color 0.3s ease, transform 0.3s ease;
}
.theme-toggle:hover {
border-color: var(--accent);
transform: rotate(20deg);
}
What does this code do?
The .navbar uses display: flex with justify-content: space-between to push the logo to the left, links to the center, and the toggle button to the right.
The position: fixed keeps the nav pinned to the top as the user scrolls. The backdrop-filter: blur(10px) creates a frosted-glass effect so content is slightly visible behind the nav.
- Save style.css and refresh your browser.
- You should see a fixed navigation bar at the top with your logo on the left, links in the middle, and the moon icon button on the right.
Nav bar not appearing fixed?
Check that the .navbar class in your CSS matches class="navbar" in your HTML. CSS class names are case-sensitive.
help me fix my navigation bar styling
- Below the navigation styles, add the hero and about section styles:
/* Hero Section */
.hero {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 2rem;
}
.hero h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
.highlight {
color: var(--accent);
}
.subtitle {
font-size: 1.4rem;
color: var(--text-secondary);
min-height: 2rem;
margin-bottom: 2rem;
}
.cta-button {
display: inline-block;
padding: 0.8rem 2rem;
background-color: var(--accent);
color: #ffffff;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background-color 0.3s ease, transform 0.3s ease;
}
.cta-button:hover {
background-color: var(--accent-hover);
transform: translateY(-2px);
}
/* About Section */
.about {
padding: 5rem 2rem;
max-width: 800px;
margin: 0 auto;
text-align: center;
}
.about h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
color: var(--text-primary);
}
.about p {
font-size: 1.1rem;
color: var(--text-secondary);
}
What does this code do?
The .hero uses Flexbox with min-height: 100vh to fill the full browser window and center everything vertically and horizontally. This creates a dramatic landing area.
The .cta-button has a transition so it lifts slightly on hover, giving the user tactile feedback. The about section uses max-width: 800px with margin: 0 auto to keep text readable and centered.
- Save and refresh your browser.
- Your hero section now fills the entire viewport with your name centered and a purple "View My Work" button below it.
Hero not filling the full screen?
Make sure the * { margin: 0; padding: 0; } reset is at the top of your file. Without it, the default body margin adds unwanted space.
help me debug my hero section layout
- Below the about section styles, add the projects grid and card styles:
/* Projects Section */
.projects {
padding: 5rem 2rem;
background-color: var(--bg-secondary);
}
.projects h2 {
font-size: 2rem;
text-align: center;
margin-bottom: 3rem;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.project-card {
background-color: var(--card-bg);
padding: 2rem;
border-radius: 12px;
box-shadow: var(--card-shadow);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.project-card:hover {
transform: translateY(-5px);
box-shadow: var(--card-shadow-hover);
}
.project-card h3 {
font-size: 1.3rem;
margin-bottom: 0.8rem;
color: var(--text-primary);
}
.project-card p {
color: var(--text-secondary);
margin-bottom: 1rem;
}
.project-tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.tag {
padding: 0.3rem 0.8rem;
background-color: var(--bg-secondary);
border-radius: 20px;
font-size: 0.85rem;
color: var(--accent);
font-weight: 500;
}
What does this code do?
The .projects-grid uses CSS Grid with repeat(auto-fit, minmax(300px, 1fr)). This creates a responsive grid that automatically adjusts the number of columns based on available space. Each card is at least 300px wide.
The .project-card:hover lifts cards with translateY(-5px) and intensifies the shadow, creating a "popping out" effect when the user hovers.
Why CSS Grid instead of Flexbox here?
Flexbox works great for one-dimensional layouts like the navbar (a single row). Grid is better for two-dimensional layouts like a card gallery where you want consistent rows AND columns.
The auto-fit keyword makes the grid responsive without needing media queries. It automatically wraps cards to new rows as the browser shrinks.
- Save and refresh your browser.
- You should see three project cards arranged side by side in a grid. Hover over a card to see it lift up with a shadow effect.
Cards stacking vertically instead of side by side?
Your browser window may be too narrow for three 300px cards. Try making the window wider. If they still stack, check that .projects-grid has display: grid and the grid-template-columns property is spelled correctly.
help me fix my CSS Grid layout
- Below the tag styles, add the contact section and footer styles:
/* Contact Section */
.contact {
padding: 5rem 2rem;
text-align: center;
}
.contact h2 {
font-size: 2rem;
margin-bottom: 1rem;
}
.contact p {
color: var(--text-secondary);
margin-bottom: 2rem;
font-size: 1.1rem;
}
.contact-links {
display: flex;
justify-content: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.contact-link {
padding: 0.7rem 1.5rem;
border: 2px solid var(--accent);
color: var(--accent);
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background-color 0.3s ease, color 0.3s ease, transform 0.3s ease;
}
.contact-link:hover {
background-color: var(--accent);
color: #ffffff;
transform: translateY(-2px);
}
/* Footer */
footer {
text-align: center;
padding: 2rem;
border-top: 1px solid var(--border);
color: var(--text-secondary);
}
What does this code do?
The .contact-link buttons are styled with a border-only ("ghost button") look that fills with color on hover. The flex-wrap: wrap ensures links wrap to a new line on smaller screens.
The footer has a subtle top border to visually separate it from the content above.
- Save and refresh your browser.
- Scroll down past the projects. You should see three purple-outlined contact buttons (GitHub, LinkedIn, Email) and a footer with a border above it.
Contact links not appearing as buttons?
Verify that .contact-link in your CSS matches class="contact-link" in your HTML. Also confirm the border: 2px solid var(--accent) property is present.
help me style my contact section links
Add responsive breakpoints and animations
Your site looks great on a wide screen, but it needs to adapt for mobile devices. You'll also add animation classes that JavaScript will trigger in the next step.
- Below the footer styles, add the scroll-reveal animation classes:
/* Scroll Reveal Animation */
.reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.active {
opacity: 1;
transform: translateY(0);
}
What does this code do?
The .reveal class starts elements as invisible and shifted 30px down. When JavaScript adds the .active class, the element fades in and slides up smoothly thanks to the transition property.
Your HTML already has class="reveal" on the about, projects, and contact sections. JavaScript will add .active when they scroll into view.
- Save and refresh your browser.
- Your About, Projects, and Contact sections now disappear. This is expected. They have opacity: 0 and will reappear in the next step when you add JavaScript.
Worried your sections vanished?
This is correct behavior. The .reveal class intentionally hides those sections. In Step 4, you'll write JavaScript that adds the .active class to make them visible as you scroll.
If you want to temporarily see your sections to verify styling, add .active to the class list in your HTML (e.g. class="about reveal active"). Just remember to remove it before moving on.
- Below the reveal classes, add the responsive media query:
/* Responsive Design */
@media (max-width: 768px) {
.hero h1 {
font-size: 2rem;
}
.subtitle {
font-size: 1.1rem;
}
.navbar {
padding: 1rem;
}
.nav-links {
gap: 1rem;
}
.nav-links a {
font-size: 0.9rem;
}
.projects-grid {
grid-template-columns: 1fr;
}
}
What does this code do?
The @media (max-width: 768px) rule only applies when the browser window is 768 pixels wide or smaller (roughly tablet/phone size).
Inside the media query, font sizes shrink, navigation spacing tightens, and the project grid switches to a single column with grid-template-columns: 1fr. This ensures the site is readable on mobile without horizontal scrolling.
What is responsive design?
Responsive design means your website adapts its layout to different screen sizes. Media queries let you write CSS rules that only activate at specific widths. Combined with the flexible auto-fit grid from earlier, your site works on everything from phones to ultrawide monitors.
- Save style.css and resize your browser window to be narrow (like a phone screen).
- You should see the hero heading shrink and the navigation links tighten their spacing. The project cards will stack into a single column.
Responsive styles not kicking in?
Make sure the @media block is at the very bottom of your style.css file. Media queries need to come after the base styles they override.
help me debug my responsive media query
✔️ Awesome, I've got everything!
Great. Double check you've saved style.css and your browser shows the styled hero section with the responsive grid below.
ⓧ I'd like to double check the full code
Here's the complete style.css file. Compare it with yours to find any differences:
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #1a1a2e;
--text-secondary: #4a4a6a;
--accent: #6c63ff;
--accent-hover: #5a52d5;
--card-bg: #ffffff;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--card-shadow-hover: 0 8px 25px rgba(108, 99, 255, 0.2);
--border: #e0e0e0;
--nav-bg: rgba(255, 255, 255, 0.95);
--font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
color-scheme: light dark;
}
[data-theme="dark"] {
--bg-primary: #0f0f1a;
--bg-secondary: #1a1a2e;
--text-primary: #e0e0e0;
--text-secondary: #a0a0b0;
--accent: #7c73ff;
--accent-hover: #9b94ff;
--card-bg: #1a1a2e;
--card-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
--card-shadow-hover: 0 8px 25px rgba(124, 115, 255, 0.3);
--border: #2a2a3e;
--nav-bg: rgba(15, 15, 26, 0.95);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-main);
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Navigation */
.navbar {
position: fixed;
top: 0;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: var(--nav-bg);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
z-index: 1000;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--accent);
text-decoration: none;
}
.nav-links {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-links a {
text-decoration: none;
color: var(--text-primary);
font-weight: 500;
transition: color 0.3s ease;
}
.nav-links a:hover {
color: var(--accent);
}
.theme-toggle {
background: none;
border: 2px solid var(--border);
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 1.2rem;
cursor: pointer;
transition: border-color 0.3s ease, transform 0.3s ease;
}
.theme-toggle:hover {
border-color: var(--accent);
transform: rotate(20deg);
}
/* Hero Section */
.hero {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 2rem;
}
.hero h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
.highlight {
color: var(--accent);
}
.subtitle {
font-size: 1.4rem;
color: var(--text-secondary);
min-height: 2rem;
margin-bottom: 2rem;
}
.cta-button {
display: inline-block;
padding: 0.8rem 2rem;
background-color: var(--accent);
color: #ffffff;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background-color 0.3s ease, transform 0.3s ease;
}
.cta-button:hover {
background-color: var(--accent-hover);
transform: translateY(-2px);
}
/* About Section */
.about {
padding: 5rem 2rem;
max-width: 800px;
margin: 0 auto;
text-align: center;
}
.about h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
color: var(--text-primary);
}
.about p {
font-size: 1.1rem;
color: var(--text-secondary);
}
/* Projects Section */
.projects {
padding: 5rem 2rem;
background-color: var(--bg-secondary);
}
.projects h2 {
font-size: 2rem;
text-align: center;
margin-bottom: 3rem;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.project-card {
background-color: var(--card-bg);
padding: 2rem;
border-radius: 12px;
box-shadow: var(--card-shadow);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.project-card:hover {
transform: translateY(-5px);
box-shadow: var(--card-shadow-hover);
}
.project-card h3 {
font-size: 1.3rem;
margin-bottom: 0.8rem;
color: var(--text-primary);
}
.project-card p {
color: var(--text-secondary);
margin-bottom: 1rem;
}
.project-tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.tag {
padding: 0.3rem 0.8rem;
background-color: var(--bg-secondary);
border-radius: 20px;
font-size: 0.85rem;
color: var(--accent);
font-weight: 500;
}
/* Contact Section */
.contact {
padding: 5rem 2rem;
text-align: center;
}
.contact h2 {
font-size: 2rem;
margin-bottom: 1rem;
}
.contact p {
color: var(--text-secondary);
margin-bottom: 2rem;
font-size: 1.1rem;
}
.contact-links {
display: flex;
justify-content: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.contact-link {
padding: 0.7rem 1.5rem;
border: 2px solid var(--accent);
color: var(--accent);
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background-color 0.3s ease, color 0.3s ease, transform 0.3s ease;
}
.contact-link:hover {
background-color: var(--accent);
color: #ffffff;
transform: translateY(-2px);
}
/* Footer */
footer {
text-align: center;
padding: 2rem;
border-top: 1px solid var(--border);
color: var(--text-secondary);
}
/* Scroll Reveal Animation */
.reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.active {
opacity: 1;
transform: translateY(0);
}
/* Responsive Design */
@media (max-width: 768px) {
.hero h1 {
font-size: 2rem;
}
.subtitle {
font-size: 1.1rem;
}
.navbar {
padding: 1rem;
}
.nav-links {
gap: 1rem;
}
.nav-links a {
font-size: 0.9rem;
}
.projects-grid {
grid-template-columns: 1fr;
}
}
Your portfolio is styled and responsive. Next up, you'll bring it to life with JavaScript. You'll build a dark mode toggle, a typing animation, and scroll-reveal effects that make sections fade in as visitors scroll.
Add JavaScript Interactivity
Your portfolio looks polished with responsive CSS, smooth hover effects, and dark mode variables ready to go. Now it's time to bring those features to life with JavaScript.
Right now the dark mode toggle button does nothing, the subtitle area is blank, and sections appear invisible until someone interacts with them. In this step, you'll write the JavaScript that powers all three of those features.
In this step, get ready to:
- Build a dark/light mode toggle that remembers the user's preference.
- Create a typing animation effect that cycles through phrases.
- Add scroll-reveal animations using IntersectionObserver.
Build the dark/light mode toggle
Your CSS already defines a [data-theme="dark"] selector with dark color variables. The JavaScript needs to toggle that attribute on and off, and remember the user's choice across page reloads using localStorage.
- In VS Code, open script.js (currently empty) and paste the following code:
// Dark Mode Toggle
const themeToggle = document.getElementById('theme-toggle');
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
themeToggle.textContent = savedTheme === 'dark' ? '☀️' : '🌙';
}
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeToggle.textContent = newTheme === 'dark' ? '☀️' : '🌙';
});
What does this code do?
This code connects the moon button in your nav bar to the dark mode CSS you already wrote.
- document.getElementById('theme-toggle') grabs the button element from your HTML.
- localStorage.getItem('theme') checks if the user previously chose dark mode. If they did, the page applies dark mode immediately on load.
- The click listener reads the current theme, flips it, saves the new preference to localStorage, and swaps the button icon between a moon and sun.
- Save script.js.
- Switch to your browser. Click the moon button in the top-right corner of the nav bar. Your entire page should flip to dark colors.
- Refresh the page. The dark theme should persist because the preference is saved in localStorage.
Clicking the button does nothing?
- Make sure you saved script.js. Live Server only picks up saved changes.
- Open DevTools with F12 and check the Console tab for errors. A typo in getElementById will show a null reference error.
- Confirm your HTML button has id="theme-toggle" (not just the class name).
My dark mode toggle isn't working. How do I debug it?
Create the typing effect
The hero section has an empty <p> element with id="typing-text". You'll fill it with an animated typing effect that cycles through different roles, making the hero section feel alive.
- Add the following code below your dark mode toggle in script.js:
// Typing Effect
const typingElement = document.getElementById('typing-text');
const phrases = [
'Software Engineering Student',
'Web Developer',
'Problem Solver',
'Quick Learner'
];
let phraseIndex = 0;
let charIndex = 0;
let isDeleting = false;
let typingSpeed = 100;
function typeEffect() {
const currentPhrase = phrases[phraseIndex];
if (isDeleting) {
typingElement.textContent = currentPhrase.substring(0, charIndex - 1);
charIndex--;
typingSpeed = 50;
} else {
typingElement.textContent = currentPhrase.substring(0, charIndex + 1);
charIndex++;
typingSpeed = 100;
}
if (!isDeleting && charIndex === currentPhrase.length) {
isDeleting = true;
typingSpeed = 1500;
} else if (isDeleting && charIndex === 0) {
isDeleting = false;
phraseIndex = (phraseIndex + 1) % phrases.length;
typingSpeed = 500;
}
setTimeout(typeEffect, typingSpeed);
}
typeEffect();
What does this code do?
This creates the classic "typewriter" animation you see on many developer portfolios.
- phrases holds the strings the effect cycles through. You can customize these later.
- typeEffect() types one character at a time, pauses when a phrase is complete, then deletes character by character before moving to the next phrase.
- setTimeout(typeEffect, typingSpeed) calls itself recursively. The speed changes depending on whether it's typing (100ms), deleting (50ms), or pausing between words (1500ms).
- Save script.js.
- Switch to your browser. Below your name in the hero section, you should see text typing out "Software Engineering Student" one letter at a time, then deleting and starting the next phrase.
No typing animation appearing?
- Check the Console tab in DevTools for errors. A misspelled variable name like typingElement will break the function silently.
- Make sure the typeEffect() call appears after the function definition. Without it, the animation never starts.
- Verify your HTML has id="typing-text" on the subtitle <p> element.
Help me debug my typing animation.
Add smooth scroll and scroll-reveal
Your CSS already defines the .reveal class that starts sections as invisible. Now you need JavaScript to detect when the user scrolls a section into view and trigger the animation by adding the .active class.
What is IntersectionObserver?
IntersectionObserver is a browser API that efficiently detects when an element enters or leaves the viewport. It's the modern replacement for listening to scroll events, which can hurt performance because they fire dozens of times per second.
- Add the following code below your typing effect in script.js:
// Scroll Reveal with IntersectionObserver
const revealElements = document.querySelectorAll('.reveal');
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
}
});
}, {
threshold: 0.15
});
revealElements.forEach((element) => {
revealObserver.observe(element);
});
What does this code do?
- querySelectorAll('.reveal') finds every section you tagged with the reveal class in your HTML (About, Projects, Contact).
- The observer watches each element. When 15% of a section becomes visible (threshold: 0.15), it adds the active class.
- Your CSS transitions handle the rest. The .reveal.active rule fades the section in and slides it up from its hidden position.
- Save script.js.
- Switch to your browser and scroll down the page. Each section (About, Projects, Contact) should gracefully fade in and slide up as it enters the viewport.
Sections not animating on scroll?
- If all sections are already visible without animation, make sure your CSS has .reveal { opacity: 0; } defined. Without it, sections are never hidden.
- If sections stay invisible forever, check DevTools Console for errors. A typo in querySelectorAll or the class name will return an empty list.
- Confirm your HTML sections have class="about reveal", class="projects reveal", and class="contact reveal".
My scroll reveal animations aren't triggering. Help me debug.
Now let's confirm your full JavaScript file is correct.
✔️ Awesome, I've got everything!
Great. Double check you've saved script.js and all three features (dark mode, typing effect, scroll reveal) are working in your browser.
ⓧ I'd like to double check the full code
Here's the complete script.js file. Compare yours against it:
// Dark Mode Toggle
const themeToggle = document.getElementById('theme-toggle');
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
themeToggle.textContent = savedTheme === 'dark' ? '☀️' : '🌙';
}
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeToggle.textContent = newTheme === 'dark' ? '☀️' : '🌙';
});
// Typing Effect
const typingElement = document.getElementById('typing-text');
const phrases = [
'Software Engineering Student',
'Web Developer',
'Problem Solver',
'Quick Learner'
];
let phraseIndex = 0;
let charIndex = 0;
let isDeleting = false;
let typingSpeed = 100;
function typeEffect() {
const currentPhrase = phrases[phraseIndex];
if (isDeleting) {
typingElement.textContent = currentPhrase.substring(0, charIndex - 1);
charIndex--;
typingSpeed = 50;
} else {
typingElement.textContent = currentPhrase.substring(0, charIndex + 1);
charIndex++;
typingSpeed = 100;
}
if (!isDeleting && charIndex === currentPhrase.length) {
isDeleting = true;
typingSpeed = 1500;
} else if (isDeleting && charIndex === 0) {
isDeleting = false;
phraseIndex = (phraseIndex + 1) % phrases.length;
typingSpeed = 500;
}
setTimeout(typeEffect, typingSpeed);
}
typeEffect();
// Scroll Reveal with IntersectionObserver
const revealElements = document.querySelectorAll('.reveal');
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
}
});
}, {
threshold: 0.15
});
revealElements.forEach((element) => {
revealObserver.observe(element);
});
Your portfolio is now fully interactive. The dark mode toggle remembers visitor preferences, phrases type themselves in the hero, and sections animate into view on scroll. Next up, you'll deploy this live to the internet with Git and GitHub Pages.
Deploy to GitHub Pages
Your portfolio is fully built and looking great in the browser. The final step is getting it live on the internet so anyone can visit it.
You'll use Git to track your files and push them to GitHub. Then you'll enable GitHub Pages to turn your repository into a live website with its own URL.
In this step, get ready to:
- Initialize Git and make your first commit.
- Push your code to GitHub.
- Enable GitHub Pages and verify your live site.
Initialize Git and make your first commit
Git needs to know that your portfolio folder is a project it should track. Initializing a repository tells Git to start watching for changes in this folder.
- Open the VS Code integrated terminal by pressing Ctrl+` (that's the backtick key, located above the Tab key).
- Initialize a Git repository, stage all your files, and create your first commit by running these commands:
git init
git add .
git commit -m "Initial commit: portfolio website"
What do these commands do?
- git init creates a new Git repository in your current folder. This tells Git to start tracking file changes here.
- git add . stages all files in the folder (the dot means "everything"). Staging marks files as ready to be saved in the next commit.
- git commit -m "..." saves a snapshot of your staged files with a descriptive message. This is your first checkpoint.
- You should see output confirming that 3 files were committed.
Seeing an error about user identity?
If Git says it doesn't know who you are, your identity wasn't configured in Step 1. Run the config commands from that step again, then retry the commit.
Help me troubleshoot my Git commit error.
Push to GitHub
Your commit only exists on your computer right now. Pushing sends your files to the remote repository on GitHub so they're accessible online.
- Connect your local repository to GitHub and push your code by running these commands:
git remote add origin https://github.com/[[GITHUB_USERNAME="your GitHub username"]]/[[GITHUB_USERNAME="your GitHub username"]].github.io.git
git branch -M main
git push -u origin main
What do these commands do?
- git remote add origin links your local repository to the remote one on GitHub. Both username placeholders will be replaced with your actual GitHub username.
- git branch -M main renames your default branch to main. GitHub Pages expects this branch name.
- git push -u origin main uploads your committed files to GitHub. The -u flag sets this remote as the default for future pushes.
What is the authentication pop-up?
Git may open a browser window asking you to sign in to GitHub. Follow the prompts to authorize VS Code to access your account. This only happens the first time you push.
- After authentication completes, you should see output confirming your branch was pushed successfully.
- Go to your repository on GitHub in Chrome. You should see your three files (index.html, style.css, script.js) listed on the repository page.
Push failed or authentication error?
Make sure the repository URL matches your actual GitHub username. Double-check that your repository is set to Public and that you did NOT add a README file when creating it.
Help me fix my push error.
Enable GitHub Pages and verify your live site
GitHub Pages turns your repository into a hosted website. You just need to tell GitHub which branch and folder to serve files from.
- On your repository page on GitHub, click Settings in the top menu bar.
- In the left sidebar, click Pages.
- Under Build and deployment, set Source to Deploy from a branch.
- Set Branch to main and the folder to / (root).
- Click Save.
How long until my site is live?
GitHub Pages can take up to 10 minutes to publish your site for the first time. Typically it takes 1-2 minutes. You can check the build status on the Pages settings page.
- Wait 1-2 minutes, then visit your site in a new browser tab:
https://[[GITHUB_USERNAME="your GitHub username"]].github.io
- You should see your full portfolio website live on the internet. The hero section, typing animation, project cards, and dark mode toggle should all work exactly as they did locally.
Seeing a 404 page?
Make sure your repository is named exactly yourusername.github.io (with your actual username). Also confirm that index.html is at the root of your repository, not inside a subfolder.
If it has been more than 10 minutes, go back to Settings then Pages and confirm the branch is set to main with folder / (root).
Help me troubleshoot my 404 error.
Your portfolio is now live on the internet for anyone to see. Any time you push changes to the main branch, your site updates automatically within minutes.
Secret mission
Add a Filterable Projects Section
Your portfolio shows all your projects at once. What if a recruiter only wants to see your Python work, or just your web projects? Add category filter buttons that let visitors instantly narrow down what they see.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on free services. GitHub Pages hosts your site at no cost, and there are no running processes or paid cloud resources to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- GitHub repository: yourusername.github.io (free, no charges).
- GitHub Pages site: hosted for free with 100 GB/month bandwidth.
- Local portfolio folder on your Desktop with index.html, style.css, and script.js.
- Git installed on your system via winget.
Keep everything running
No action needed. Choose this if you want your portfolio to stay live and you plan to keep updating it.
- GitHub Pages is completely free. Your site stays live at your yourusername.github.io URL indefinitely.
- Any time you push changes to the main branch, your site updates automatically within minutes.
- Git remains installed on your machine. You will use it for virtually every future coding project.
Pause - I'll come back to this later
Take the site offline temporarily while keeping all your code intact. You can re-enable it at any time.
- Go to your repository on GitHub.
- Click Settings, then select Pages from the left sidebar.
- Click the three-dot menu next to your live site URL and select Unpublish site.
- Your code and repository stay safe. When you are ready to publish again, return to Settings > Pages and set the source branch back to main.
Delete - I don't want to use this again
Remove all project resources and start fresh. This is permanent.
- Delete the GitHub repository (this also removes your live GitHub Pages site):
- Go to your repository on GitHub.
- Click Settings.
- Scroll to the bottom of the page to the Danger Zone section.
- Click Delete this repository and follow the confirmation prompts.
Delete your local project files by running this command in your terminal:
macOS
rm -rf ~/Desktop/portfolio
Windows
Remove-Item -Recurse -Force ~\Desktop\portfolio
Git is a fundamental development tool you will likely use in every future project. If you still want to remove it:
- Open Windows Settings.
- Select Apps, then Installed apps.
- Search for Git, click the three-dot menu, and select Uninstall.
Nice Work!
Nice Work!
You just built and deployed a professional portfolio website from scratch. You now have a live URL to share on your resume, LinkedIn, and job applications.
You learned how to:
- Built a responsive portfolio with semantic HTML5, CSS Grid and Flexbox layouts, custom properties for theming, and a dark/light mode toggle that persists with localStorage.
- Added JavaScript interactivity including a typing animation effect, smooth scroll navigation, and scroll-reveal animations powered by IntersectionObserver.
- Learned Git version control basics (init, add, commit, push) and deployed a live website to GitHub Pages.
- Added filterable project categories with JavaScript DOM manipulation so visitors can sort your work by type.
Ready to quiz yourself?