Table of Contents
1. Setting Up Your React Project
First, let's scaffold a new React project using Vite (recommended) or Create React App. Vite is significantly faster and produces a smaller build.
Using Vite (Recommended)
# Create a new React project
npm create vite@latest portfolio -- --template react
# Navigate into the project
cd portfolio
# Install dependencies
npm install
# Start the dev server
npm run dev
Using Create React App
npx create-react-app portfolio
cd portfolio
npm start
After running the dev server, you'll see your app at http://localhost:5173 (Vite) or http://localhost:3000 (CRA).
Initial Folder Structure
portfolio/
├── public/
│ └── favicon.jpg
├── src/
│ ├── assets/
│ ├── App.jsx
│ ├── App.css
│ ├── index.css
│ └── main.jsx
├── index.html
├── package.json
└── vite.config.js
2. Project Structure & Components
A clean folder structure keeps your project maintainable as it grows. Organize by feature, not by file type.
Recommended Folder Structure
src/
├── components/
│ ├── Navbar.jsx
│ ├── Hero.jsx
│ ├── About.jsx
│ ├── Skills.jsx
│ ├── Projects.jsx
│ ├── Contact.jsx
│ └── Footer.jsx
├── data/
│ └── portfolio.js
├── styles/
│ ├── global.css
│ ├── navbar.css
│ ├── hero.css
│ └── ...
├── App.jsx
├── App.css
└── main.jsx
App.jsx — The Main Layout
import Navbar from './components/Navbar'
import Hero from './components/Hero'
import About from './components/About'
import Skills from './components/Skills'
import Projects from './components/Projects'
import Contact from './components/Contact'
import Footer from './components/Footer'
function App() {
return (
<div className="App">
<Navbar />
<Hero />
<About />
<Skills />
<Projects />
<Contact />
<Footer />
</div>
)
}
export default App
3. Building the Hero Section
The hero section is the first thing visitors see. Let's build one with a typewriter effect using useState and useEffect.
Hero.jsx
import { useState, useEffect } from 'react'
const titles = [
'Frontend Developer',
'React Engineer',
'UI/UX Enthusiast',
'Problem Solver'
]
export default function Hero() {
const [currentTitle, setCurrentTitle] = useState(0)
const [displayText, setDisplayText] = useState('')
const [isDeleting, setIsDeleting] = useState(false)
useEffect(() => {
const fullText = titles[currentTitle]
let timeout
if (!isDeleting && displayText === fullText) {
timeout = setTimeout(() => setIsDeleting(true), 2000)
} else if (isDeleting && displayText === '') {
setIsDeleting(false)
setCurrentTitle((prev) => (prev + 1) % titles.length)
} else {
timeout = setTimeout(() => {
setDisplayText(
isDeleting
? fullText.substring(0, displayText.length - 1)
: fullText.substring(0, displayText.length + 1)
)
}, isDeleting ? 50 : 100)
}
return () => clearTimeout(timeout)
}, [displayText, isDeleting, currentTitle])
return (
<section className="hero" id="home">
<div className="hero-container">
<h1 className="hero-name">
Hi, I'm <span className="highlight">Avinash</span>
</h1>
<h2 className="hero-title">
{displayText}
<span className="cursor">|</span>
</h2>
<p className="hero-desc">
I build modern, performant web applications with React.
</p>
<div className="hero-buttons">
<a href="#projects" className="btn btn-primary">
View My Work
</a>
<a href="#contact" className="btn btn-secondary">
Get In Touch
</a>
</div>
</div>
</section>
)
}
CSS for the Typewriter Cursor
.cursor {
animation: blink 1s step-end infinite;
color: #6366f1;
font-weight: 300;
}
@keyframes blink {
50% { opacity: 0; }
}
4. Creating the About Section
The About section uses component composition and props to render a reusable layout.
About.jsx
export default function About() {
const highlights = [
{ icon: 'fa-code', label: 'Clean Code' },
{ icon: 'fa-rocket', label: 'Performance' },
{ icon: 'fa-palette', label: 'UI/UX Design' },
{ icon: 'fa-vial', label: 'Testing' },
]
return (
<section className="about" id="about">
<div className="container">
<h2 className="section-title">About Me</h2>
<div className="about-content">
<div className="about-text">
<p>
I'm a frontend developer specializing in React
and modern JavaScript. I love building intuitive
interfaces and writing clean, testable code.
</p>
<p>
With experience in component-driven development,
state management, and end-to-end testing, I focus
on delivering reliable, performant web applications.
</p>
</div>
<div className="about-highlights">
{highlights.map((item, index) => (
<HighlightCard key={index} {...item} />
))}
</div>
</div>
</div>
</section>
)
}
function HighlightCard({ icon, label }) {
return (
<div className="highlight-card">
<i className={`fas ${icon}`}></i>
<span>{label}</span>
</div>
)
}
{...item} passes all object properties as individual props. It's clean and avoids repetitive prop drilling.
5. Skills Section with Progress Bars
Display your skills as animated progress bars using array mapping and CSS transitions.
Skills.jsx
import { useState, useEffect, useRef } from 'react'
const skillsData = [
{ name: 'React', level: 90, color: '#61DAFB' },
{ name: 'JavaScript', level: 88, color: '#F7DF1E' },
{ name: 'TypeScript', level: 80, color: '#3178C6' },
{ name: 'CSS/SASS', level: 85, color: '#CC6699' },
{ name: 'Node.js', level: 70, color: '#68A063' },
{ name: 'Testing (Jest/Cypress)', level: 82, color: '#22C55E' },
]
export default function Skills() {
const [visible, setVisible] = useState(false)
const sectionRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true)
observer.disconnect()
}
},
{ threshold: 0.3 }
)
if (sectionRef.current) observer.observe(sectionRef.current)
return () => observer.disconnect()
}, [])
return (
<section className="skills" id="skills" ref={sectionRef}>
<div className="container">
<h2 className="section-title">Skills</h2>
<div className="skills-grid">
{skillsData.map((skill, index) => (
<div className="skill-item" key={index}>
<div className="skill-header">
<span className="skill-name">{skill.name}</span>
<span className="skill-percent">{skill.level}%</span>
</div>
<div className="skill-bar">
<div
className="skill-progress"
style={{
width: visible ? `${skill.level}%` : '0%',
backgroundColor: skill.color,
transitionDelay: `${index * 0.1}s`,
}}
></div>
</div>
</div>
))}
</div>
</div>
</section>
)
}
Progress Bar CSS
.skill-bar {
background: rgba(255, 255, 255, 0.08);
border-radius: 10px;
height: 10px;
overflow: hidden;
}
.skill-progress {
height: 100%;
border-radius: 10px;
width: 0;
transition: width 1.2s ease-out;
}
6. Projects Showcase
Display projects in a responsive card grid with conditional rendering for tags and links.
Projects.jsx
const projectsData = [
{
title: 'E-Commerce Dashboard',
description: 'A real-time admin dashboard built with React and Chart.js for monitoring sales and inventory.',
tags: ['React', 'Chart.js', 'Firebase'],
github: 'https://github.com/avinashbt/ecommerce-dash',
live: 'https://ecommerce-demo.vercel.app',
image: '/projects/ecommerce.jpg',
},
{
title: 'Testing Framework Guide',
description: 'Interactive guide for learning Cypress and Jest with runnable code examples.',
tags: ['React', 'Cypress', 'Jest'],
github: 'https://github.com/avinashbt/testing-guide',
live: null,
image: '/projects/testing.jpg',
},
]
export default function Projects() {
return (
<section className="projects" id="projects">
<div className="container">
<h2 className="section-title">Projects</h2>
<div className="projects-grid">
{projectsData.map((project, index) => (
<div className="project-card" key={index}>
<img
src={project.image}
alt={project.title}
className="project-image"
/>
<div className="project-info">
<h3>{project.title}</h3>
<p>{project.description}</p>
<div className="project-tags">
{project.tags.map((tag, i) => (
<span key={i} className="tag">{tag}</span>
))}
</div>
<div className="project-links">
{project.github && (
<a href={project.github} target="_blank" rel="noreferrer">
<i className="fab fa-github"></i> Code
</a>
)}
{project.live && (
<a href={project.live} target="_blank" rel="noreferrer">
<i className="fas fa-external-link-alt"></i> Live
</a>
)}
</div>
</div>
</div>
))}
</div>
</div>
</section>
)
}
{project.live && ...} prevents rendering an empty anchor tag.
7. Contact Form with Validation
A contact form with client-side validation using useState and email regex.
Contact.jsx
import { useState } from 'react'
export default function Contact() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: '',
})
const [errors, setErrors] = useState({})
const [submitted, setSubmitted] = useState(false)
const validateEmail = (email) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
const validate = () => {
const newErrors = {}
if (!formData.name.trim()) newErrors.name = 'Name is required'
if (!formData.email.trim()) {
newErrors.email = 'Email is required'
} else if (!validateEmail(formData.email)) {
newErrors.email = 'Invalid email address'
}
if (!formData.message.trim()) newErrors.message = 'Message is required'
return newErrors
}
const handleChange = (e) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: '' }))
}
}
const handleSubmit = (e) => {
e.preventDefault()
const newErrors = validate()
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
// Send form data to your backend or email service
console.log('Form submitted:', formData)
setSubmitted(true)
setFormData({ name: '', email: '', subject: '', message: '' })
}
if (submitted) {
return (
<section className="contact" id="contact">
<div className="container">
<div className="success-message">
<i className="fas fa-check-circle"></i>
<h3>Message Sent!</h3>
<p>Thank you for reaching out. I'll reply soon.</p>
</div>
</div>
</section>
)
}
return (
<section className="contact" id="contact">
<div className="container">
<h2 className="section-title">Get In Touch</h2>
<form className="contact-form" onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className={errors.name ? 'error' : ''}
/>
{errors.name && <span className="error-text">{errors.name}</span>}
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-text">{errors.email}</span>}
</div>
<div className="form-group">
<label htmlFor="subject">Subject</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
rows="5"
value={formData.message}
onChange={handleChange}
className={errors.message ? 'error' : ''}
></textarea>
{errors.message && <span className="error-text">{errors.message}</span>}
</div>
<button type="submit" className="btn btn-primary">
<i className="fas fa-paper-plane"></i> Send Message
</button>
</form>
</div>
</section>
)
}
8. Adding Animations
Scroll-based animations make your portfolio feel dynamic. Here are two approaches: AOS library and custom IntersectionObserver.
Option A: AOS (Animate on Scroll)
# Install AOS
npm install aos
# Import in main.jsx
import AOS from 'aos'
import 'aos/dist/aos.css'
AOS.init({
duration: 800,
easing: 'ease-out-cubic',
once: true,
})
<!-- Use in JSX -->
<div data-aos="fade-up">
<h2>About Me</h2>
</div>
<div data-aos="fade-left" data-aos-delay="200">
<p>Animated paragraph</p>
</div>
Option B: Custom CSS Animations
/* Define animation keyframes */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Apply with IntersectionObserver */
.animate-in {
animation: fadeInUp 0.6s ease-out forwards;
}
useInView Custom Hook
import { useState, useEffect, useRef } from 'react'
export function useInView(options = {}) {
const ref = useRef(null)
const [isInView, setIsInView] = useState(false)
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsInView(true)
observer.disconnect()
}
}, { threshold: 0.1, ...options })
if (ref.current) observer.observe(ref.current)
return () => observer.disconnect()
}, [])
return [ref, isInView]
}
// Usage
function AnimatedSection({ children }) {
const [ref, isInView] = useInView()
return (
<div
ref={ref}
className={isInView ? 'animate-in' : 'opacity-0'}
>
{children}
</div>
)
}
transform and opacity for animations — they're GPU-accelerated and won't cause layout reflows. Avoid animating width, height, or margin.
9. Deploying to Vercel or Netlify
Once your portfolio is ready, deploy it for the world to see.
Deploy to Vercel
# Install Vercel CLI
npm install -g vercel
# Login
vercel login
# Deploy (from project root)
vercel
# Deploy to production
vercel --prod
Deploy to Netlify
# Install Netlify CLI
npm install -g netlify-cli
# Login
netlify login
# Initialize and deploy
netlify init
netlify deploy --prod
GitHub Actions Auto-Deploy
# .github/workflows/deploy.yml
name: Deploy to Vercel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
- run: npm run build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
| Feature | Vercel | Netlify |
|---|---|---|
| Free Tier | Unlimited deploys | 100GB bandwidth |
| Build Speed | Very fast | Fast |
| Serverless Functions | Built-in | Built-in |
| Custom Domains | Free SSL | Free SSL |
| Framework Support | First-class React/Next.js | Good React support |