Build a Portfolio Website
Build and deploy a responsive React portfolio site with Vite.
Introduction
30 Second Summary
Every time you finish a web project, you want a place to show it off. A single link you can share with friends, recruiters, or clients that says "here's what I've built."
In this project, you will build a responsive portfolio website using Vite and React, then deploy it to a live URL that anyone can visit.
What You'll Build
You'll open your portfolio URL on your phone, resize the browser on desktop, and watch your project cards flow from a single column into a polished multi-column grid.
By the end of this project, you'll have:
- A live portfolio URL you can share with anyone, where each of your websites appears as a visual card with a title, description, and clickable link.
- A reusable component architecture where adding a new project takes 30 seconds of editing a single data file.
- A responsive grid layout that automatically adjusts from one column on mobile to multiple columns on desktop.
- Secret Mission: Add category filtering so visitors can filter your projects by type.
Are there any prerequisites?
You should have basic familiarity with HTML, CSS, and JavaScript. You'll also need Node.js installed on your machine (version 20.19+ or 22.12+).
No prior React experience is required. This project teaches you as you build.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
Taking a moment to commit to the goal helps you stay oriented as you move through each step.
Set Up Your Vite React Project
Building a portfolio means constantly tweaking layouts, text, and colors until everything feels right. You need a development environment that shows changes the instant you save a file.
Vite is a build tool designed for speed. It gives you hot module replacement, so your browser updates without a full page reload. In this step, you will scaffold a React project with Vite and confirm it runs locally.
In this step, get ready to:
- Scaffold a new Vite + React project on your Desktop.
- Install the project dependencies.
- Start the development server and see the starter page in your browser.
Scaffold the Vite React project
The first thing you need is an open terminal inside VS Code.
- Open the integrated terminal by pressing Ctrl+` (macOS and Windows). The backtick key is above the Tab key.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold your portfolio project by running this command:
npm create vite@latest my-portfolio -- --template react
What will the terminal ask you?
Because you provided the --template react flag, Vite skips the framework selection. It will still ask two things:
- If npm asks to install the create-vite package, type y and press Enter.
- When it asks Which linter to use? select Oxlint (the first option) and press Enter.
- When it asks Install with npm and start now? select No. You will install manually in the next command so you can see each piece clearly.
After the prompts finish, you will see a message confirming the project was scaffolded.
- Move into the new project folder and install dependencies by running these commands:
cd my-portfolio
npm install
You should see output ending with a line like "added 186 packages" (the exact number may vary).
Seeing permission errors or command not found?
- Make sure you ran cd ~/Desktop before the scaffold command. If my-portfolio was created elsewhere, delete it and start from the Desktop.
- On Windows, if you see a script execution policy error, run Set-ExecutionPolicy RemoteSigned -Scope CurrentUser in PowerShell, then retry.
Help me troubleshoot my npm install errors
Start the development server
- Start the Vite dev server by running:
npm run dev
What does this command do?
This starts Vite's local development server. It watches your files for changes and pushes updates to the browser instantly, without a full page reload.
The command takes over this terminal session. You will see a URL printed (http://localhost:5173) indicating the server is running. When you need to stop it later, press Ctrl+C.
Confirm the starter page loads
Before you open the URL, predict what you think you will see. A blank page? An error? Something else?
- Open http://localhost:5173 in your browser.
You should see the Vite + React starter page with a spinning React logo and a counter button that increments when you click it.
Don't see the starter page?
- Check that your terminal still shows the Vite server running. If you accidentally closed it, run npm run dev again from inside the my-portfolio folder.
- Make sure the URL is exactly http://localhost:5173 with no trailing path. If port 5173 was already in use, Vite picks the next available port and prints it in the terminal output.
Help me figure out why my Vite dev server page is not loading
✔️ Awesome, I've got everything!
Great. Make sure you keep the dev server running in your terminal. You will use it for the rest of the project.
ⓧ I'd like to double check the full code
Open src/main.jsx in VS Code. It should match this exactly:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
If yours differs, delete the my-portfolio folder and re-run the scaffold command from the beginning of this step.
Your development environment is ready and running. Next up, you will build your first project card component and see it appear on screen.
Build Your First Project Card
Your Vite development environment is running and ready. Now it's time to replace that default starter page with something that's yours: a project card that showcases one of your websites.
In this step, you'll create a reusable React component that displays a project's title, description, screenshot, and a link to the live site. The goal is to see something real on screen before thinking about architecture.
In this step, get ready to:
- Create a ProjectCard component with its own styles.
- Render a single card in your app using hardcoded data.
- Replace the default styles with a clean CSS reset.
Create the ProjectCard component
Every project in your portfolio will be displayed as a card. In React, you build reusable UI pieces as components. You'll create a ProjectCard component that accepts data through props and renders a styled card.
- In the VS Code terminal, create a new folder for your components by running this command:
mkdir src/components
- Create a new file called ProjectCard.jsx inside src/components/ and add the following code:
import './ProjectCard.css'
function ProjectCard({ title, description, url, image }) {
return (
<div className="project-card">
<div className="project-card__image">
<img src={image} alt={`Screenshot of ${title}`} />
</div>
<div className="project-card__content">
<h2 className="project-card__title">{title}</h2>
<p className="project-card__description">{description}</p>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="project-card__link"
>
Visit Site
</a>
</div>
</div>
)
}
export default ProjectCard
What does this code do?
This is a React component that receives four props (title, description, url, image) and returns JSX that structures a card with an image area, a content section, and a link button.
The target="_blank" attribute on the link opens the site in a new tab. The rel="noopener noreferrer" attribute is a security best practice that prevents the new page from accessing your site's window object.
Next, you need to create the CSS file that this component imports. Without it, Vite will show an error overlay in the browser.
- Create a new file called ProjectCard.css inside src/components/ and add these card container and image styles:
.project-card {
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.project-card__image {
width: 100%;
height: 200px;
overflow: hidden;
background: #f0f0f0;
}
.project-card__image img {
width: 100%;
height: 100%;
object-fit: cover;
}
What does this code do?
These styles shape the outer shell of your card. The .project-card class gives it rounded corners, a subtle shadow, and a hover animation that lifts the card up. The image container is fixed at 200px tall, with object-fit: cover to crop images proportionally.
The gray #f0f0f0 background on the image area acts as a placeholder if your screenshot hasn't loaded or doesn't exist yet.
- Confirm both files exist in the src/components/ folder by running:
ls src/components/
You should see ProjectCard.css and ProjectCard.jsx listed in the output.
- Now add the content and link styles below the existing code in ProjectCard.css:
.project-card__content {
padding: 1.5rem;
}
.project-card__title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #1a1a1a;
}
.project-card__description {
font-size: 0.95rem;
color: #555;
margin-bottom: 1rem;
line-height: 1.5;
}
.project-card__link {
display: inline-block;
padding: 0.5rem 1.25rem;
background: #646cff;
color: #ffffff;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
transition: background 0.2s ease;
}
.project-card__link:hover {
background: #535bf2;
}
What does this code do?
These styles handle the text content area inside each card. The title gets a bold weight, the description uses a lighter gray for visual hierarchy, and the link is styled as a purple button with a hover color shift.
- Save ProjectCard.css.
✔️ Awesome, I've got everything!
Great. Make sure you've saved ProjectCard.jsx and ProjectCard.css. Nothing will appear in the browser yet because App.jsx hasn't imported your component. You'll fix that next.
ⓧ I'd like to double check the full code
Your ProjectCard.css is built from two chunks. Scroll up and confirm you have both sections in this order:
- Card container and image styles (.project-card, .project-card:hover, .project-card__image, .project-card__image img).
- Content and link styles (.project-card__content, .project-card__title, .project-card__description, .project-card__link, .project-card__link:hover).
Seeing an error in the terminal?
If you see a file-not-found error, make sure both files are inside src/components/ (not src/ directly). The import path in ProjectCard.jsx expects ./ProjectCard.css to be in the same folder.
help me fix a file import error in my Vite React project
Render the card in your app
Your component exists but your app doesn't know about it yet. You need to import and render it in App.jsx to see the card in the browser.
- Replace the entire contents of src/App.jsx with the following code:
import ProjectCard from './components/ProjectCard'
import './App.css'
function App() {
return (
<div className="portfolio">
<ProjectCard
title="My First Project"
description="A brief description of your first website project."
url="https://your-first-site.com"
image="/screenshots/project-one.png"
/>
</div>
)
}
export default App
What does this code do?
This imports your ProjectCard component and renders it with hardcoded props. Each prop passes a piece of data into the card: the title, a short description, the URL the link button opens, and a path to a screenshot image.
The className="portfolio" wrapper will be styled next to center the card on the page.
- Save App.jsx and check your browser.
The Vite starter page is gone. In its place you should see your project card with a gray image area, a title, description text, and a purple "Visit Site" button.
Don't see the card?
Make sure you saved App.jsx. Vite's hot reload picks up changes on save. If the browser shows an error overlay, check that the import path matches exactly: './components/ProjectCard'. The path is case-sensitive.
help me debug why my ProjectCard component isn't showing up
- Now replace the entire contents of src/App.css with these centering styles:
.portfolio {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
What does this code do?
This removes all the default Vite styles and replaces them with a simple centered container. The margin: 0 auto centers the portfolio horizontally, and max-width: 1200px prevents it from stretching across ultra-wide screens.
- Save App.css and check the browser. Your card should now sit neatly in the top-left of a centered container with some padding around it.
Replace the default global styles
The Vite starter includes default global styles in index.css that interfere with your layout. You'll replace them with a clean CSS reset that gives you a neutral starting point.
- Replace the entire contents of src/index.css with this minimal reset:
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, sans-serif;
background: #f5f5f5;
color: #1a1a1a;
min-height: 100vh;
}
a {
color: inherit;
text-decoration: none;
}
What does this code do?
The universal selector * strips all default margins and padding from every element, giving you full control over spacing. The box-sizing: border-box rule makes width calculations predictable by including padding in an element's total width.
The body gets a light gray background (#f5f5f5) and a system font stack that uses each operating system's native font. This ensures fast rendering without downloading web fonts.
Before you save, what do you expect to change in the browser?
- Save index.css and check your browser.
The page background shifts to a soft gray. Your white card now stands out against it with clear contrast. The font has switched to your system's native typeface.
Card looks the same as before?
If the background didn't change, make sure you replaced the entire contents of src/index.css (not src/App.css). Vite imports index.css in main.jsx, so it applies globally to the entire page.
help me figure out why my CSS reset isn't applying
✔️ Awesome, I've got everything!
Great. Confirm you've saved all files and your browser shows a white card on a gray background with a title, description, and purple button.
ⓧ I'd like to double check the full code
Here is the complete src/App.jsx:
import ProjectCard from './components/ProjectCard'
import './App.css'
function App() {
return (
<div className="portfolio">
<ProjectCard
title="My First Project"
description="A brief description of your first website project."
url="https://your-first-site.com"
image="/screenshots/project-one.png"
/>
</div>
)
}
export default App
Here is the complete src/App.css:
.portfolio {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
Here is the complete src/index.css:
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, sans-serif;
background: #f5f5f5;
color: #1a1a1a;
min-height: 100vh;
}
a {
color: inherit;
text-decoration: none;
}
For ProjectCard.jsx and ProjectCard.css, scroll up to the component substep and confirm your code matches the blocks there.
You have a visible project card on screen. Right now the data is hardcoded directly in App.jsx. Next up, you'll move your project data into a separate file so adding new cards is as simple as adding an object to an array.
Make It Data-Driven
Your portfolio card is looking sharp. You have a reusable component, solid styling, and a visible result in the browser.
But imagine you have five websites to showcase. Right now, you would need to copy-paste the entire ProjectCard tag five times and manually change every prop. That is tedious and error-prone. In this step, you will move your project data into a separate file and let React render cards automatically from that data.
In this step, get ready to:
- Create a data file that holds all your project information.
- Update your app to render cards from that data using .map().
- Prove the pattern works by adding a new project in seconds.
Create your projects data file
Instead of scattering project details across your JSX, you will store them in one dedicated file. Each project becomes an object with all the information the card needs.
- In the VS Code Explorer sidebar, right-click the src folder and select New Folder.
- Name the folder data.
- Right-click the new data folder and select New File.
- Name the file projects.js.
- Paste the following code into src/data/projects.js:
const projects = [
{
id: 1,
title: "Project One",
description: "A brief description of your first website project.",
url: "https://your-first-site.com",
image: "/screenshots/project-one.png",
category: "personal"
},
{
id: 2,
title: "Project Two",
description: "A brief description of your second website project.",
url: "https://your-second-site.com",
image: "/screenshots/project-two.png",
category: "personal"
},
{
id: 3,
title: "Project Three",
description: "A brief description of your third website project.",
url: "https://your-third-site.com",
image: "/screenshots/project-three.png",
category: "client"
}
]
export default projects
What does this code do?
Each object in the array represents one project card. The fields match the props your ProjectCard component already expects: title, description, url, and image.
The id field gives React a unique identifier for each item when rendering lists. The category field will come in handy later for filtering.
Use your real websites
Replace the placeholder titles, descriptions, and URLs with details from your actual web projects. If you do not have screenshots yet, the cards will show a gray background as a placeholder.
- Save src/data/projects.js.
- Confirm the file appears in the Explorer sidebar under src/data/projects.js.
Don't see the data folder?
Make sure you created the data folder inside src, not at the project root. The path should be src/data/projects.js.
If you accidentally created it in the wrong spot, drag the data folder into src in the Explorer sidebar.
Still stuck? Help me fix my folder structure for src/data/projects.js
Update App.jsx to render from data
Now that your projects live in a data file, you can replace the hardcoded card with a loop that renders one card per project automatically.
- Open src/App.jsx in VS Code.
- Select all the existing code and replace it with the following:
import projects from './data/projects'
import ProjectCard from './components/ProjectCard'
import './App.css'
function App() {
return (
<div className="portfolio">
<main className="portfolio__grid">
{projects.map((project) => (
<ProjectCard
key={project.id}
title={project.title}
description={project.description}
url={project.url}
image={project.image}
/>
))}
</main>
</div>
)
}
export default App
What does .map() do here?
The .map() method loops through every object in the projects array and returns a ProjectCard component for each one. React renders all of them into the page.
The key={project.id} prop tells React which card is which. This helps React update only the cards that change instead of re-rendering everything.
- Save src/App.jsx.
- Switch to your browser. You should see three project cards instead of one.
Seeing a blank page or error?
Check your terminal for error messages. The most common cause is a typo in the import path. Make sure the first line reads import projects from './data/projects' with the exact folder name.
Also confirm your projects.js file ends with export default projects. Without the export, the import in App.jsx cannot find the data.
Need help? Help me debug why my project cards aren't rendering from data
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is src/data/projects.js:
const projects = [
{
id: 1,
title: "Project One",
description: "A brief description of your first website project.",
url: "https://your-first-site.com",
image: "/screenshots/project-one.png",
category: "personal"
},
{
id: 2,
title: "Project Two",
description: "A brief description of your second website project.",
url: "https://your-second-site.com",
image: "/screenshots/project-two.png",
category: "personal"
},
{
id: 3,
title: "Project Three",
description: "A brief description of your third website project.",
url: "https://your-third-site.com",
image: "/screenshots/project-three.png",
category: "client"
}
]
export default projects
Here is src/App.jsx:
import projects from './data/projects'
import ProjectCard from './components/ProjectCard'
import './App.css'
function App() {
return (
<div className="portfolio">
<main className="portfolio__grid">
{projects.map((project) => (
<ProjectCard
key={project.id}
title={project.title}
description={project.description}
url={project.url}
image={project.image}
/>
))}
</main>
</div>
)
}
export default App
Prove it works by adding a new project
The whole point of a data-driven approach is that adding a new project means adding one object. No JSX editing, no copy-pasting components.
Before you test this, what do you think will happen when you add a fourth object to the array and save?
- Open src/data/projects.js.
- Add a new object after the third project (before the closing ]). Use this as a template:
{
id: 4,
title: "Project Four",
description: "A test project to prove data-driven rendering works.",
url: "https://example.com",
image: "/screenshots/project-four.png",
category: "personal"
}
- Make sure you add a comma after the third object's closing } so the array stays valid.
- Save the file and switch to your browser.
A fourth card appears instantly. You did not touch App.jsx or ProjectCard.jsx. That is the power of separating data from presentation.
Why does this matter?
Every future website you build gets added to your portfolio by typing one object into projects.js. The component handles how it looks. The data file handles what it shows. You never mix the two.
- Feel free to keep the fourth project or remove it. You can customize the array with your real websites at any time.
New card not appearing?
The most common cause is a missing comma between objects. Every object except the last must have a comma after its closing }.
Check your browser's developer console (press Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) for a syntax error pointing to projects.js.
Still stuck? Help me fix a syntax error in my projects data array
Your portfolio now renders any number of projects from a single data file. Next up, you will style the layout into a responsive grid that adapts from mobile to desktop.
Style the Responsive Grid Layout
Your cards are rendering project data from Step 3, but right now they stack in a single column no matter how wide the screen is. On a desktop monitor, that leaves most of the screen empty.
CSS Grid can fix this with a single layout rule that automatically adjusts column count based on available width. In this step, you will add a responsive grid and a polished header to give your portfolio a professional look.
In this step, get ready to:
- Add CSS Grid styles that automatically adjust column count based on screen width.
- Add a portfolio header with your title and tagline.
- Verify the layout responds to different browser widths.
Add the responsive grid styles
Your src/App.css currently only has a basic container rule. You need grid layout rules that tell the browser how many columns to create, and header styles to center your portfolio title above the cards.
- Replace the entire contents of src/App.css with the following styles:
.portfolio {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.portfolio__header {
text-align: center;
margin-bottom: 3rem;
}
.portfolio__header h1 {
font-size: 2.5rem;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 0.5rem;
}
.portfolio__header p {
font-size: 1.1rem;
color: #666;
}
.portfolio__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 2rem;
}
What does this CSS do?
- .portfolio__grid uses display: grid to activate CSS Grid on the card container.
- repeat(auto-fill, minmax(320px, 1fr)) tells the browser to create as many columns as fit, each at least 320px wide. On a narrow screen, only one column fits. On a wide screen, two or three appear automatically.
- gap: 2rem adds even spacing between cards without needing margins on individual cards.
- .portfolio__header centers the title and tagline text, with spacing below to separate it from the grid.
- Save src/App.css.
- Switch back to your browser. Your cards should now appear side by side in a multi-column grid instead of stacked vertically.
Cards still stacking in a single column?
- Make sure the class name in your CSS is .portfolio__grid (two underscores). It must match className="portfolio__grid" in your JSX.
- Check that your browser window is at least 700px wide. With the minmax(320px, 1fr) rule, you need enough width for two 320px columns plus the gap.
- Why aren't my grid styles applying to the cards?
✔️ Awesome, I've got everything!
Great. Double check you have saved src/App.css before moving on.
ⓧ I'd like to double check the full code
Here is the complete src/App.css at this point:
.portfolio {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.portfolio__header {
text-align: center;
margin-bottom: 3rem;
}
.portfolio__header h1 {
font-size: 2.5rem;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 0.5rem;
}
.portfolio__header p {
font-size: 1.1rem;
color: #666;
}
.portfolio__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 2rem;
}
Add a portfolio header to your layout
The grid is working, but there is no title telling visitors what this page is. You need a header section above the grid with your portfolio name and a short tagline.
- Open src/App.jsx.
- Replace the entire contents of the file with the following code, which adds a <header> section above the grid:
import projects from './data/projects'
import ProjectCard from './components/ProjectCard'
import './App.css'
function App() {
return (
<div className="portfolio">
{/* Page header with title and tagline */}
<header className="portfolio__header">
<h1>My Portfolio</h1>
<p>A collection of my web projects</p>
</header>
{/* Responsive card grid */}
<main className="portfolio__grid">
{projects.map((project) => (
<ProjectCard
key={project.id}
title={project.title}
description={project.description}
url={project.url}
image={project.image}
/>
))}
</main>
</div>
)
}
export default App
What changed here?
- The <header className="portfolio__header"> wraps a title and tagline. The CSS you just added centers this text and pushes the grid down with margin-bottom: 3rem.
- The <main> tag keeps the card grid semantically separate from the header. Screen readers use these landmarks to help users navigate.
- Save src/App.jsx.
- Switch back to your browser. You should see a large centered "My Portfolio" heading with "A collection of my web projects" below it, followed by your card grid.
Header not showing up?
- Confirm your <header> is inside the <div className="portfolio"> but before the <main> tag.
- Check the terminal for a red error. A missing closing tag or stray character in JSX will prevent the page from rendering.
- Why isn't my header rendering in the browser?
✔️ Awesome, I've got everything!
Great. Make sure you have saved src/App.jsx before continuing.
ⓧ I'd like to double check the full code
Here is the complete src/App.jsx at this point:
import projects from './data/projects'
import ProjectCard from './components/ProjectCard'
import './App.css'
function App() {
return (
<div className="portfolio">
<header className="portfolio__header">
<h1>My Portfolio</h1>
<p>A collection of my web projects</p>
</header>
<main className="portfolio__grid">
{projects.map((project) => (
<ProjectCard
key={project.id}
title={project.title}
description={project.description}
url={project.url}
image={project.image}
/>
))}
</main>
</div>
)
}
export default App
Verify the responsive layout
The layout should now adapt automatically to different screen widths. Before you test, what do you think will happen when you drag your browser window to be very narrow?
- Drag the right edge of your browser window to make it wider and narrower.
- On a wide screen (900px+), you should see 2-3 cards per row.
- On a narrow screen (below 700px), the cards should stack into a single column.
- Hover over a card. It should lift up slightly with a deeper shadow effect.
Why does auto-fill handle responsiveness without media queries?
The repeat(auto-fill, minmax(320px, 1fr)) rule tells the browser to fit as many 320px-minimum columns as it can. When the viewport is too narrow for two columns, the grid automatically falls back to one. No breakpoints needed.
Your portfolio now looks professional with a clean grid and smooth hover interactions. Next up, you will deploy it to Vercel so anyone can visit it with a link.
Deploy to Vercel
Your portfolio looks polished locally. Responsive grid, hover effects, data-driven cards. But right now, only you can see it.
A portfolio only works if people can visit it. In this step, you'll connect your local code to a live URL on Vercel that you can share with anyone.
In this step, get ready to:
- Initialize a Git repository and make your first commit.
- Deploy your portfolio to Vercel using the CLI.
- Verify your live site works on desktop and mobile.
Initialize a Git repository
Vercel deploys from Git repositories. Before deploying, you need to track your project files with Git so Vercel knows what to build.
- Your dev server is still running in the terminal from earlier. Open a new terminal in VS Code by pressing Ctrl+Shift+` (macOS and Windows).
- Confirm you are inside the my-portfolio folder by running:
pwd
You should see a path ending in my-portfolio. If not, run cd ~/Desktop/my-portfolio to navigate there.
- Initialize a Git repository and commit all your project files by running these commands:
git init
git add .
git commit -m "Initial portfolio site"
What do these commands do?
- git init creates a new Git repository in your project folder.
- git add . stages every file in the project for tracking.
- git commit -m "Initial portfolio site" saves a snapshot of your code with a descriptive message.
You should see output listing the files that were committed, with lines showing create mode followed by your file paths.
Seeing a Git error?
- If Git asks you to configure your identity, it needs your name and email. Run git config --global user.name "Your Name" and git config --global user.email "you@example.com", then try the commit again.
- If you see not a git repository, make sure you ran git init first and that your terminal is inside the my-portfolio folder.
Help me fix my Git commit error
Deploy with the Vercel CLI
The Vercel CLI can deploy your project straight from the terminal. It auto-detects Vite projects and configures the build settings for you.
- Deploy your portfolio by running this command:
npx vercel
What prompts will I see?
The Vercel CLI walks you through several interactive prompts. Here is how to answer each one:
- Set up and deploy? Press Y to confirm.
- Which scope/team? Select your personal account.
- Link to existing project? Press N to create a new one.
- What's your project's name? Press Enter to accept the default (my-portfolio).
- In which directory is your code located? Press Enter to accept ./.
- Want to modify these settings? Press N. Vercel detects Vite automatically and sets the build command and output directory for you.
Vercel builds your project in the cloud and prints a live URL when finished. This takes about 30-60 seconds.
Prompted to log in?
If Vercel opens a browser window asking you to log in, sign in with the same account you used to create your Vercel account. Once authenticated, return to your terminal and the deployment continues automatically.
Help me authenticate with the Vercel CLI
Verify your live portfolio
Before you open the URL, what do you think you'll see? Will it look any different from your local version?
- Copy the URL that Vercel printed in the terminal (it looks like https://my-portfolio-xxxxx.vercel.app).
- Open it in your browser.
You should see your full portfolio live on the internet. All your project cards, the responsive grid, and hover effects work exactly as they do locally.
- Test responsive behavior by resizing your browser window to a narrow width, or by opening the URL on your phone. Cards should stack into a single column below 320px.
What about automatic deployments?
Once you connect a GitHub repository to your Vercel project through the Vercel dashboard, every push to your main branch automatically triggers a new deployment. Your site stays up to date without running any deploy commands.
Site not loading or looking wrong?
- If you see a 404 page, the build may have failed. Run npx vercel again and watch the terminal for error messages.
- If images are missing, that is expected if you have not added screenshot files to a public/screenshots/ folder yet. The gray placeholder background appears instead.
- If the layout looks broken, make sure all your CSS files were saved before you ran git add .. Unsaved changes are not included in the commit or deployment.
Help me fix my Vercel deployment
Secret mission
Add Category Filtering
Your portfolio is growing. With more than a few projects, visitors need a way to find what they're looking for. Can you add interactive category buttons that filter your project cards in real time?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and deploys to Vercel's free Hobby plan, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Vercel deployment (free Hobby plan, no charges).
- Local my-portfolio project folder on your Desktop.
- Local Git repository inside the project folder.
Keep everything running
No action needed. Choose this if you are still actively building your portfolio or plan to add more projects soon.
- Your Vercel deployment stays live indefinitely on the free Hobby plan (up to 100 GB bandwidth per month and 100 deploys per day).
- Your local my-portfolio folder remains on your Desktop ready to edit.
- You can add a custom domain in the Vercel dashboard at any time (up to 50 domains on the Hobby plan).
Pause - I'll come back to this later
Stop the local development server to free up your terminal and memory. Your Vercel deployment continues to serve your site at no cost.
- Switch to the terminal where npm run dev is running and press Ctrl+C to stop the development server.
- Your project files and Vercel deployment remain intact. When you are ready to resume, open the my-portfolio folder and run npm run dev again.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your local files and takes down the live Vercel deployment.
- Stop the development server by pressing Ctrl+C in the terminal where it is running.
- Delete the local project folder by running this in your terminal:
macOS
cd ~/Desktop
rm -rf my-portfolio
Windows
cd ~\Desktop
Remove-Item -Recurse -Force my-portfolio
- Delete the Vercel deployment:
- Open vercel.com/dashboard and select your my-portfolio project.
- Click Settings in the sidebar.
- Scroll to the bottom of the General page and find the Delete Project section.
- Click Delete, enter the project name and confirmation phrase, then click Continue.
That's a Wrap!
That's a Wrap!
Nice work! You just built and deployed a responsive portfolio website using React and Vite, complete with a reusable component architecture and a live URL you can share with anyone.
You've learned how to:
- Build a React portfolio site from scratch using Vite, with a reusable ProjectCard component that accepts data via props.
- Implement a data-driven architecture where adding a new project to your site means adding one object to an array.
- Style a responsive grid layout using CSS Grid with auto-fill and minmax, then deploy to Vercel with a shareable live URL.
- Secret Mission: Added interactive category filtering using React state so visitors can browse projects by type.
Ready to quiz yourself?