Build a Stunning Personal Portfolio with React, Vite, and TailwindCSS

Pedro Machado / February 21, 2025
11 min read •
Description
A comprehensive step-by-step tutorial to create a beautiful, responsive personal portfolio using React, Vite, and TailwindCSS.
Build a Stunning Personal Portfolio with React, Vite, and TailwindCSS
In this tutorial, you’ll learn how to build a modern, responsive personal portfolio from scratch using React, Vite, and TailwindCSS. We’ll go through the complete code step by step, including a custom loading screen, animated reveal on scroll, a glass-effect navbar with a mobile hamburger menu (with a close button), and multiple sections (Home, About, Projects, Contact).
Watch the Full Tutorial
Prefer to watch the tutorial? Check out the full video on my YouTube channel:
Visit my channel for more web development tutorials: PedroTech YouTube Channel.
Table of Contents
- Prerequisites
- Project Setup
- Tailwind and Global Styles
- Application Structure
- Component Breakdown
- Conclusion
1. Prerequisites
Before you begin, make sure you have Node.js and npm installed on your machine. You should also have basic knowledge of React and TailwindCSS.
2. Project Setup
First, create a new React project using Vite:
npm create vite@latest my-portfolio --template react
cd my-portfolio
npm install
Then, install TailwindCSS along with PostCSS and Autoprefixer:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Configure your tailwind.config.js
to include your source files:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
3. Tailwind and Global Styles
In your src/index.css
, import Tailwind’s directives and set up some global styles:
@import "tailwindcss";
html,
body {
margin: 0;
padding: 0;
font-family: "Space Grotesk", sans-serif;
background: #0a0a0a;
color: #f3f4f6;
}
@layer utilities {
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.animate-blink {
animation: blink 0.8s step-end infinite;
}
@keyframes loading {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(250%);
}
}
.animate-loading-bar {
animation: loading 0.8s ease infinite;
}
}
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
4. Application Structure
Your project will have the following structure:
my-portfolio/
├── public/
│ └── index.html
├── src/
│ ├── components/
│ │ ├── LoadingScreen.jsx
│ │ ├── Navbar.jsx
│ │ ├── MobileMenu.jsx
│ │ ├── RevealOnScroll.jsx
│ │ └── sections/
│ │ ├── Home.jsx
│ │ ├── About.jsx
│ │ ├── Projects.jsx
│ │ └── Contact.jsx
│ ├── App.jsx
│ ├── index.css
│ └── App.css
├── package.json
├── tailwind.config.js
└── vite.config.js
5. Component Breakdown
Loading Screen
This component shows a loading screen with a typing effect and a loading bar.
// src/components/LoadingScreen.jsx
import { useEffect, useState } from "react";
export const LoadingScreen = ({ onComplete }) => {
const [text, setText] = useState("");
const fullText = "<Hello Wolrd />";
useEffect(() => {
let index = 0;
const interval = setInterval(() => {
setText(fullText.substring(0, index));
index++;
if (index > fullText.length) {
clearInterval(interval);
setTimeout(() => {
onComplete();
}, 1000);
}
}, 100);
return () => clearInterval(interval);
}, [onComplete]);
return (
<div className="fixed inset-0 z-50 bg-black text-gray-100 flex flex-col items-center justify-center">
<div className="mb-4 text-4xl font-mono font-bold">
{text} <span className="animate-blink ml-1">|</span>
</div>
<div className="w-[200px] h-[2px] bg-gray-800 rounded relative overflow-hidden">
<div className="w-[40%] h-full bg-blue-500 shadow-[0_0_15px_#3b82f6] animate-loading-bar"></div>
</div>
</div>
);
};
Navbar
The Navbar has a glass-effect background, a logo, and a hamburger menu for mobile. When clicked, the hamburger toggles the mobile menu.
// src/components/Navbar.jsx
import { useEffect } from "react";
export const Navbar = ({ menuOpen, setMenuOpen }) => {
useEffect(() => {
document.body.style.overflow = menuOpen ? "hidden" : "";
}, [menuOpen]);
return (
<nav className="fixed top-0 w-full z-40 bg-[rgba(10,10,10,0.8)] backdrop-blur-lg border-b border-white/10 shadow-lg">
<div className="max-w-5xl mx-auto px-4">
<div className="flex justify-between items-center h-16">
<a href="#home" className="font-mono text-xl font-bold text-white">
pedro<span className="text-blue-500">.tech</span>
</a>
<div
className="w-7 h-5 relative cursor-pointer z-40 md:hidden"
onClick={() => setMenuOpen((prev) => !prev)}
>
☰
</div>
<div className="hidden md:flex items-center space-x-8">
<a
href="#home"
className="text-gray-300 hover:text-white transition-colors"
>
Home
</a>
<a
href="#about"
className="text-gray-300 hover:text-white transition-colors"
>
About
</a>
<a
href="#projects"
className="text-gray-300 hover:text-white transition-colors"
>
Projects
</a>
<a
href="#contact"
className="text-gray-300 hover:text-white transition-colors"
>
Contact
</a>
</div>
</div>
</div>
</nav>
);
};
Mobile Menu
The Mobile Menu slides in when the hamburger is clicked and includes a close button.
// src/components/MobileMenu.jsx
import { useEffect } from "react";
export const MobileMenu = ({ menuOpen, setMenuOpen }) => {
return (
<div
className={`
fixed top-0 left-0 w-full bg-[rgba(10,10,10,0.8)] z-40 flex flex-col items-center justify-center
transition-all duration-300 ease-in-out
${
menuOpen
? "h-screen opacity-100 pointer-events-auto"
: "h-0 opacity-0 pointer-events-none"
}
`}
>
<button
onClick={() => setMenuOpen(false)}
className="absolute top-6 right-6 text-white text-3xl focus:outline-none cursor-pointer"
aria-label="Close Menu"
>
×
</button>
<a
href="#home"
onClick={() => setMenuOpen(false)}
className="text-2xl font-semibold text-white my-4 transform transition-transform duration-300 hover:text-blue-500 hover:scale-110"
>
Home
</a>
<a
href="#about"
onClick={() => setMenuOpen(false)}
className="text-2xl font-semibold text-white my-4 transform transition-transform duration-300 hover:text-blue-500 hover:scale-110"
>
About
</a>
<a
href="#projects"
onClick={() => setMenuOpen(false)}
className="text-2xl font-semibold text-white my-4 transform transition-transform duration-300 hover:text-blue-500 hover:scale-110"
>
Projects
</a>
<a
href="#contact"
onClick={() => setMenuOpen(false)}
className="text-2xl font-semibold text-white my-4 transform transition-transform duration-300 hover:text-blue-500 hover:scale-110"
>
Contact
</a>
</div>
);
};
Reveal On Scroll
This component uses the Intersection Observer API to reveal elements as they come into view.
// src/components/RevealOnScroll.jsx
import { useEffect, useRef } from "react";
export const RevealOnScroll = ({ children }) => {
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
ref.current.classList.add("visible");
}
},
{ threshold: 0.2, rootMargin: "0px 0px -50px 0px" }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<div ref={ref} className="reveal">
{children}
</div>
);
};
Home Section
The Home section introduces your portfolio with a bold heading, a brief description, and call-to-action buttons.
// src/components/sections/Home.jsx
import { RevealOnScroll } from "../RevealOnScroll";
export const Home = () => {
return (
<section
id="home"
className="min-h-screen flex items-center justify-center relative"
>
<RevealOnScroll>
<div className="text-center z-10 px-4">
<h1 className="text-5xl md:text-7xl font-bold mb-6 bg-gradient-to-r from-blue-500 to-cyan-400 bg-clip-text text-transparent leading-right">
Hi, I'm Pedro Tech
</h1>
<p className="text-gray-400 text-lg mb-8 max-w-lg mx-auto">
I’m a full-stack developer who loves crafting clean, scalable web
applications. My goal is to build solutions that offer both
exceptional performance and a delightful user experience.
</p>
<div className="flex justify-center space-x-4">
<a
href="#projects"
className="bg-blue-500 text-white py-3 px-6 rounded font-medium transition relative overflow-hidden hover:-translate-y-0.5 hover:shadow-[0_0_15px_rgba(59,130,246,0.4)]"
>
View Projects
</a>
<a
href="#contact"
className="border border-blue-500/50 text-blue-500 py-3 px-6 rounded font-medium transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_0_15px_rgba(59,130,246,0.2)] hover:bg-blue-500/10"
>
Contact Me
</a>
</div>
</div>
</RevealOnScroll>
</section>
);
};
About Section
The About section provides an overview of your skills, education, and work experience, complete with interactive hover effects.
// src/components/sections/About.jsx
import { RevealOnScroll } from "../RevealOnScroll";
export const About = () => {
const frontendSkills = [
"React",
"Vue",
"TypeScript",
"TailwindCSS",
"Svelte",
];
const backendSkills = ["Node.js", "Python", "AWS", "MongoDB", "GraphQL"];
return (
<section
id="about"
className="min-h-screen flex items-center justify-center py-20"
>
<RevealOnScroll>
<div className="max-w-3xl mx-auto px-4">
<h2 className="text-3xl font-bold mb-8 bg-gradient-to-r from-blue-500 to-cyan-400 bg-clip-text text-transparent text-center">
About Me
</h2>
<div className="rounded-xl p-8 border border-white/10 hover:-translate-y-1 transition-all">
<p className="text-gray-300 mb-6">
Passionate developer with expertise in building scalable web
applications and creating innovative solutions.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="rounded-xl p-6 hover:-translate-y-1 transition-all">
<h3 className="text-xl font-bold mb-4">Frontend</h3>
<div className="flex flex-wrap gap-2">
{frontendSkills.map((tech, key) => (
<span
key={key}
className="bg-blue-500/10 text-blue-500 py-1 px-3 rounded-full text-sm hover:bg-blue-500/20 hover:shadow-[0_2px_8px_rgba(59,130,246,0.2)] transition"
>
{tech}
</span>
))}
</div>
</div>
<div className="rounded-xl p-6 hover:-translate-y-1 transition-all">
<h3 className="text-xl font-bold mb-4">Backend</h3>
<div className="flex flex-wrap gap-2">
{backendSkills.map((tech, key) => (
<span
key={key}
className="bg-blue-500/10 text-blue-500 py-1 px-3 rounded-full text-sm hover:bg-blue-500/20 hover:shadow-[0_2px_8px_rgba(59,130,246,0.2)] transition"
>
{tech}
</span>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
<div className="p-6 rounded-xl border border-white/10 hover:-translate-y-1 transition-all">
<h3 className="text-xl font-bold mb-4">🏫 Education</h3>
<ul className="list-disc list-inside text-gray-300 space-y-2">
<li>
<strong>B.S. in Computer Science</strong> - XYZ University
(2016-2020)
</li>
<li>
Relevant Coursework: Data Structures, Web Development, Cloud
Computing...
</li>
</ul>
</div>
<div className="p-6 rounded-xl border border-white/10 hover:-translate-y-1 transition-all">
<h3 className="text-xl font-bold mb-4">💼 Work Experience</h3>
<div className="space-y-4 text-gray-300">
<div>
<h4 className="font-semibold">
Software Engineer at ABC Corp (2020 - Present)
</h4>
<p>
Developed and maintained microservices for cloud-based
applications.
</p>
</div>
<div>
<h4 className="font-semibold">
Intern at DEF Startups (2019)
</h4>
<p>
Assisted in building front-end components and integration of
REST APIs.
</p>
</div>
</div>
</div>
</div>
</div>
</RevealOnScroll>
</section>
);
};
Projects Section
The Projects section showcases featured projects with details, technologies used, and a call-to-action button.
// src/components/sections/Projects.jsx
import { RevealOnScroll } from "../RevealOnScroll";
export const Projects = () => {
return (
<section
id="projects"
className="min-h-screen flex items-center justify-center py-20"
>
<RevealOnScroll>
<div className="max-w-5xl mx-auto px-4">
<h2 className="text-3xl font-bold mb-8 bg-gradient-to-r from-blue-500 to-cyan-400 bg-clip-text text-transparent text-center">
Featured Projects
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-6 rounded-xl border border-white/10 hover:-translate-y-1 hover:border-blue-500/30 hover:shadow-[0_2px_8px_rgba(59,130,246,0.2)] transition">
<h3 className="text-xl font-bold mb-2">Cloud Platform</h3>
<p className="text-gray-400 mb-4">
Scalable cloud infrastructure management with real-time
monitoring and automated scaling.
</p>
<div className="flex flex-wrap gap-2 mb-4">
{["React", "Node.js", "AWS", "Docker"].map((tech, key) => (
<span
key={key}
className="bg-blue-500/10 text-blue-500 py-1 px-3 rounded-full text-sm hover:bg-blue-500/20 hover:shadow-[0_2px_8px_rgba(59,130,246,0.1)] transition"
>
{tech}
</span>
))}
</div>
<div className="flex justify-between items-center">
<a
href="#"
className="text-blue-400 hover:text-blue-300 transition-colors my-4"
>
View Project →
</a>
</div>
</div>
<div className="p-6 rounded-xl border border-white/10 hover:-translate-y-1 hover:border-blue-500/30 hover:shadow-[0_4px_20px_rgba(59,130,246,0.1)] transition">
<h3 className="text-xl font-bold mb-2">AI Analytics Dashboard</h3>
<p className="text-gray-400 mb-4">
ML-powered data visualization platform with predictive analytics
and interactive reports.
</p>
<div className="flex flex-wrap gap-2 mb-4">
{["Python", "TensorFlow", "D3.js", "Flask"].map((tech, key) => (
<span
key={key}
className="bg-blue-500/10 text-blue-500 py-1 px-3 rounded-full text-sm transition hover:bg-blue-500/20 hover:-translate-y-0.5 hover:shadow-[0_2px_8px_rgba(59,130,246,0.2)]"
>
{tech}
</span>
))}
</div>
<div className="flex justify-between items-center">
<a
href="#"
className="text-blue-400 hover:text-blue-300 transition-colors my-4"
>
View Project →
</a>
</div>
</div>
<div className="p-6 rounded-xl border border-white/10 hover:-translate-y-1 hover:border-blue-500/30 hover:shadow-[0_4px_20px_rgba(59,130,246,0.1)] transition">
<h3 className="text-xl font-bold mb-2">E-Commerce Web App</h3>
<p className="text-gray-400 mb-4">
Full-stack e-commerce with modern UI, secure payment
integration, and customizable product inventory.
</p>
<div className="flex flex-wrap gap-2 mb-4">
{["Next.js", "TypeScript", "Stripe", "PostgreSQL"].map(
(tech) => (
<span
key={tech}
className="bg-blue-500/10 text-blue-500 py-1 px-3 rounded-full text-sm transition hover:bg-blue-500/20 hover:-translate-y-0.5 hover:shadow-[0_2px_8px_rgba(59,130,246,0.2)]"
>
{tech}
</span>
)
)}
</div>
<div className="flex justify-between items-center">
<a
href="#"
className="text-blue-400 hover:text-blue-300 transition-colors my-4"
>
View Project →
</a>
</div>
</div>
<div className="p-6 rounded-xl border border-white/10 hover:-translate-y-1 hover:border-blue-500/30 hover:shadow-[0_4px_20px_rgba(59,130,246,0.1)] transition">
<h3 className="text-xl font-bold mb-2">Real-Time Chat App</h3>
<p className="text-gray-400 mb-4">
Scalable chat platform supporting real-time messaging, presence,
and group chat features.
</p>
<div className="flex flex-wrap gap-2 mb-4">
{["Socket.IO", "Express", "React", "Redis"].map((tech, key) => (
<span
key={key}
className="bg-blue-500/10 text-blue-500 py-1 px-3 rounded-full text-sm transition hover:bg-blue-500/20 hover:-translate-y-0.5 hover:shadow-[0_2px_8px_rgba(59,130,246,0.2)]"
>
{tech}
</span>
))}
</div>
<div className="flex justify-between items-center">
<a
href="#"
className="text-blue-400 hover:text-blue-300 transition-colors my-4"
>
View Project →
</a>
</div>
</div>
</div>
</div>
</RevealOnScroll>
</section>
);
};
Contact Section
The Contact section includes a form that uses EmailJS to send messages.
// src/components/sections/Contact.jsx
import { useState } from "react";
import { RevealOnScroll } from "../RevealOnScroll";
import emailjs from "emailjs-com";
export const Contact = () => {
const [formData, setFormData] = useState({
name: "",
email: "",
message: "",
});
const handleSubmit = (e) => {
e.preventDefault();
emailjs
.sendForm(
import.meta.env.VITE_SERVICE_ID,
import.meta.env.VITE_TEMPLATE_ID,
e.target,
import.meta.env.VITE_PUBLIC_KEY
)
.then((result) => {
alert("Message Sent!");
setFormData({ name: "", email: "", message: "" });
})
.catch(() => alert("Oops! Something went wrong. Please try again."));
};
return (
<section
id="contact"
className="min-h-screen flex items-center justify-center py-20"
>
<RevealOnScroll>
<div className="px-4 w-full min-w-[300px] md:w-[500px] sm:w-2/3 p-6">
<h2 className="text-3xl font-bold mb-8 bg-gradient-to-r from-blue-500 to-cyan-400 bg-clip-text text-transparent text-center">
Get In Touch
</h2>
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="relative">
<input
type="text"
id="name"
name="name"
required
value={formData.name}
className="w-full bg-white/5 border border-white/10 rounded px-4 py-3 text-white transition focus:outline-none focus:border-blue-500 focus:bg-blue-500/5"
placeholder="Name..."
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
/>
</div>
<div className="relative">
<input
type="email"
id="email"
name="email"
required
value={formData.email}
className="w-full bg-white/5 border border-white/10 rounded px-4 py-3 text-white transition focus:outline-none focus:border-blue-500 focus:bg-blue-500/5"
placeholder="example@gmail.com"
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
/>
</div>
<div className="relative">
<textarea
id="message"
name="message"
required
rows={5}
value={formData.message}
className="w-full bg-white/5 border border-white/10 rounded px-4 py-3 text-white transition focus:outline-none focus:border-blue-500 focus:bg-blue-500/5"
placeholder="Your Message..."
onChange={(e) =>
setFormData({ ...formData, message: e.target.value })
}
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-3 px-6 rounded font-medium transition relative overflow-hidden hover:-translate-y-0.5 hover:shadow-[0_0_15px_rgba(59,130,246,0.4)]"
>
Send Message
</button>
</form>
</div>
</RevealOnScroll>
</section>
);
};
6. Conclusion
Congratulations! You’ve built a modern, responsive personal portfolio using React, Vite, and TailwindCSS. This project features a custom loading screen, a glass-effect navbar with a responsive hamburger menu, smooth reveal animations on scroll, and dynamic sections for Home, About, Projects, and Contact.
By following this tutorial step by step and studying the code, you now have a solid foundation to further customize and enhance your portfolio to showcase your skills and projects.
For more tutorials and video guides, check out my YouTube channel Pedrotechnologies.
Happy coding and enjoy building your dream portfolio!