Back to Blog

React Portfolio Website Tutorial for Beginners

Build a complete, modern portfolio from scratch using React — from project setup to deployment.

React Portfolio Website Tutorial for Beginners

A comprehensive, step-by-step guide to building a professional developer portfolio with React — complete with real code, animations, and deployment tips.

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
Why Vite over CRA? Vite offers instant HMR (Hot Module Replacement), faster builds, and native ES module support. Create React App is deprecated and no longer maintained by the React team.

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
Component rule of thumb: If a section of your page has its own logic or UI, extract it into its own component. Components should be small, focused, and reusable.

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>
  )
}
Props spreading: Using {...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;
}
IntersectionObserver: We use it to trigger the animation only when the skills section scrolls into view, creating a nice reveal effect without any external library.

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>
  )
}
Always use conditional rendering for optional links: If a project doesn't have a live demo, don't render the link. Using {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>
  )
}
Clear errors on change: By clearing the error for a field when the user starts typing again, you provide immediate feedback that their correction is being recognized.

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>
  )
}
Performance tip: Use 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'
FeatureVercelNetlify
Free TierUnlimited deploys100GB bandwidth
Build SpeedVery fastFast
Serverless FunctionsBuilt-inBuilt-in
Custom DomainsFree SSLFree SSL
Framework SupportFirst-class React/Next.jsGood React support
Post-deployment checklist: Test all links, verify images load, check responsive behavior on mobile, validate meta tags, and run Lighthouse for performance scores.
Back to Top