DEV Community

Cover image for Building a Modern Portfolio with Tailwind CSS v4, React, and Vite
Benjamin Koimett
Benjamin Koimett

Posted on

Building a Modern Portfolio with Tailwind CSS v4, React, and Vite

github : https://github.com/bkoimett/bkoimett-portofolio.git

Looking to build a sleek, modern portfolio that stands out? In this comprehensive guide, I'll walk you through creating a professional portfolio from scratch using the latest Tailwind CSS v4, React, and Vite. We'll build a fully responsive site with dark mode support and backend integration.

πŸš€ Why This Stack?

  • Vite: Lightning-fast build tool with instant hot module replacement
  • React: Component-based architecture for reusable UI elements
  • Tailwind CSS v4: Utility-first CSS with JIT compilation and dark mode support
  • React Router: Seamless client-side navigation
  • Axios: Promise-based HTTP client for API integration

πŸ“‹ Prerequisites

  • Node.js (v18 or higher)
  • npm or yarn
  • Basic knowledge of React and Tailwind

πŸ› οΈ Step-by-Step Implementation

1. Project Initialization

# Create a new Vite project with React
npm create vite@latest my-portfolio -- --template react
cd my-portfolio

# Install dependencies
npm install tailwindcss @tailwindcss/vite @vitejs/plugin-react
npm install react-router-dom axios

# Start development server
npm run dev
Enter fullscreen mode Exit fullscreen mode

2. Configure Vite for Tailwind v4

vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
  ],
})
Enter fullscreen mode Exit fullscreen mode

3. Set Up Global Styles

src/index.css

@import "tailwindcss";

/* Custom theme variables */
@theme {
  --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
}

/* Dark mode support */
:root {
  color-scheme: light;
}

:root.dark {
  color-scheme: dark;
}

/* Smooth theme transitions */
* {
  transition-property: background-color, border-color, color, fill, stroke;
  transition-duration: 200ms;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
Enter fullscreen mode Exit fullscreen mode

4. Theme Context for Dark/Light Mode

src/context/ThemeContext.jsx

import React, { createContext, useState, useEffect, useContext } from 'react';

const ThemeContext = createContext();

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};

export const ThemeProvider = ({ children }) => {
  const [isDark, setIsDark] = useState(() => {
    const savedTheme = localStorage.getItem('theme');
    if (!savedTheme) {
      return window.matchMedia('(prefers-color-scheme: dark)').matches;
    }
    return savedTheme === 'dark';
  });

  useEffect(() => {
    if (isDark) {
      document.documentElement.classList.add('dark');
      localStorage.setItem('theme', 'dark');
    } else {
      document.documentElement.classList.remove('dark');
      localStorage.setItem('theme', 'light');
    }
  }, [isDark]);

  const toggleTheme = () => setIsDark(!isDark);

  return (
    <ThemeContext.Provider value={{ isDark, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

5. Key Components

Theme Toggle Button - src/components/ThemeToggle.jsx

import React from 'react';
import { useTheme } from '../context/ThemeContext';

const ThemeToggle = () => {
  const { isDark, toggleTheme } = useTheme();

  return (
    <button
      onClick={toggleTheme}
      className="fixed top-6 right-6 p-3 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors z-50 shadow-md"
      aria-label="Toggle theme"
    >
      {isDark ? (
        <svg className="w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
          <path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
        </svg>
      ) : (
        <svg className="w-5 h-5 text-gray-700" fill="currentColor" viewBox="0 0 20 20">
          <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
        </svg>
      )}
    </button>
  );
};

export default ThemeToggle;
Enter fullscreen mode Exit fullscreen mode

6. Building the Main Pages

Home Page - src/pages/Home.jsx

import React from 'react';

const Home = () => {
  return (
    <div className="min-h-screen flex items-center justify-center bg-white dark:bg-gray-900">
      <div className="max-w-3xl mx-auto px-6 text-center">
        <h1 className="text-5xl md:text-6xl font-light text-gray-900 dark:text-white mb-6 tracking-tight">
          Alex Chen
        </h1>
        <p className="text-xl text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto">
          Software Engineer specializing in Cloud Infrastructure, DevOps, 
          and Embedded Systems
        </p>

        <div className="flex flex-wrap justify-center gap-3 mb-12">
          {['☁️ AWS', 'πŸš€ Kubernetes', 'πŸ’» React', 'πŸ”§ Embedded C', 'πŸ“¦ Terraform', 'πŸ€– CI/CD'].map((skill) => (
            <span
              key={skill}
              className="px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full text-sm font-medium"
            >
              {skill}
            </span>
          ))}
        </div>

        <div className="flex justify-center space-x-4">
          <a 
            href="/projects" 
            className="px-6 py-3 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors font-medium"
          >
            View My Work
          </a>
          <a 
            href="/about" 
            className="px-6 py-3 border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:border-gray-900 dark:hover:border-white hover:text-gray-900 dark:hover:text-white transition-colors font-medium"
          >
            About Me
          </a>
        </div>
      </div>
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

Projects Page with Filtering - src/pages/Projects.jsx

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const Projects = () => {
  const [projects, setProjects] = useState([]);
  const [filter, setFilter] = useState('all');
  const [loading, setLoading] = useState(true);

  const categories = ['all', 'Cloud', 'DevOps', 'Web Dev', 'Embedded'];

  useEffect(() => {
    const fetchProjects = async () => {
      try {
        // Replace with your actual API endpoint
        const response = await axios.get('http://localhost:3001/api/projects');
        setProjects(response.data);
      } catch (error) {
        console.error('Error fetching projects:', error);
        // Fallback sample data
        setProjects([
          {
            id: 1,
            title: 'Cloud Infrastructure Automation',
            description: 'Scalable AWS infrastructure using Terraform and Kubernetes',
            category: 'Cloud',
            image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=600&auto=format',
            technologies: ['Terraform', 'AWS', 'K8s'],
          },
          // Add more projects...
        ]);
      } finally {
        setLoading(false);
      }
    };

    fetchProjects();
  }, []);

  const filteredProjects = filter === 'all' 
    ? projects 
    : projects.filter(p => p.category === filter);

  return (
    <div className="min-h-screen bg-white dark:bg-gray-900 pt-24 pb-16">
      <div className="max-w-6xl mx-auto px-6">
        <h2 className="text-3xl font-light text-gray-900 dark:text-white mb-8">
          Featured Projects
        </h2>

        {/* Filter buttons */}
        <div className="flex flex-wrap gap-2 mb-10">
          {categories.map((cat) => (
            <button
              key={cat}
              onClick={() => setFilter(cat)}
              className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
                filter === cat
                  ? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900'
                  : 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
              }`}
            >
              {cat === 'all' ? 'All Projects' : cat}
            </button>
          ))}
        </div>

        {/* Projects grid */}
        {loading ? (
          <div className="text-center py-12">
            <div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-300 dark:border-gray-600 border-t-gray-900 dark:border-t-white"></div>
          </div>
        ) : (
          <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
            {filteredProjects.map((project) => (
              <div
                key={project.id}
                className="group bg-gray-50 dark:bg-gray-800 rounded-xl overflow-hidden hover:shadow-xl transition-all duration-300"
              >
                <div className="overflow-hidden">
                  <img
                    src={project.image}
                    alt={project.title}
                    className="w-full h-56 object-cover group-hover:scale-105 transition-transform duration-500"
                  />
                </div>
                <div className="p-6">
                  <div className="flex items-center justify-between mb-3">
                    <span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
                      {project.category}
                    </span>
                    <div className="flex gap-2">
                      {project.technologies.slice(0, 3).map((tech) => (
                        <span
                          key={tech}
                          className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded"
                        >
                          {tech}
                        </span>
                      ))}
                    </div>
                  </div>
                  <h3 className="text-xl font-medium text-gray-900 dark:text-white mb-2">
                    {project.title}
                  </h3>
                  <p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
                    {project.description}
                  </p>
                  <button className="text-sm font-medium text-gray-900 dark:text-white hover:underline">
                    View Case Study β†’
                  </button>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

export default Projects;
Enter fullscreen mode Exit fullscreen mode

7. Main App Component

src/App.jsx

import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ThemeProvider } from './context/ThemeContext';
import Navbar from './components/Navbar';
import ThemeToggle from './components/ThemeToggle';
import Home from './pages/Home';
import Projects from './pages/Projects';
import About from './pages/About';

function App() {
  return (
    <ThemeProvider>
      <Router>
        <div className="min-h-screen bg-white dark:bg-gray-900">
          <Navbar />
          <ThemeToggle />
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/projects" element={<Projects />} />
            <Route path="/about" element={<About />} />
          </Routes>
        </div>
      </Router>
    </ThemeProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

πŸ”Œ Backend Integration

To make the portfolio dynamic, you can set up a simple Express backend:

server/index.js

const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());
app.use(express.json());

// Projects API endpoint
app.get('/api/projects', (req, res) => {
  const projects = [
    {
      id: 1,
      title: 'Cloud Infrastructure Automation',
      description: 'Scalable AWS infrastructure using Terraform and Kubernetes',
      category: 'Cloud',
      image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=600&auto=format',
      technologies: ['Terraform', 'AWS', 'K8s'],
    },
    // Add more projects...
  ];
  res.json(projects);
});

app.listen(3001, () => {
  console.log('Server running on port 3001');
});
Enter fullscreen mode Exit fullscreen mode

✨ Key Features Implemented

  1. Dark/Light Mode Toggle - Persistent theme switching with system preference detection
  2. Responsive Design - Mobile-first approach using Tailwind's responsive utilities
  3. Project Filtering - Category-based filtering with smooth transitions
  4. Loading States - Spinner indicators for async operations
  5. Backend Ready - Axios integration for API calls
  6. Smooth Animations - Hover effects and transitions
  7. Clean Navigation - React Router with active link highlighting

πŸš€ Deployment

Build for Production

npm run build
Enter fullscreen mode Exit fullscreen mode

Deploy to Vercel (Recommended)

npm install -g vercel
vercel
Enter fullscreen mode Exit fullscreen mode

Deploy to Netlify

npm run build
# Drag and drop the 'dist' folder to Netlify
Enter fullscreen mode Exit fullscreen mode

πŸ“ˆ SEO Optimization Tips

  1. Add meta tags in index.html:
<meta name="description" content="Portfolio of Alex Chen - Software Engineer specializing in Cloud, DevOps, and Embedded Systems">
<meta name="keywords" content="software engineer, portfolio, cloud, devops, react, tailwind">
Enter fullscreen mode Exit fullscreen mode
  1. Use semantic HTML (header, main, section, article)
  2. Add alt text to all images
  3. Implement proper heading hierarchy (h1, h2, h3)

🎨 Customization Ideas

  • Add a blog section with MDX support
  • Implement a contact form with EmailJS
  • Add smooth scroll animations with Framer Motion
  • Include a resume download button
  • Add testimonials carousel
  • Implement portfolio item modals

πŸ“š Resources

πŸ’‘ Pro Tips

  1. Performance: Use lazy loading for images and code splitting for routes
  2. Accessibility: Ensure proper contrast ratios and keyboard navigation
  3. Testing: Add unit tests for components using Jest and React Testing Library
  4. Analytics: Integrate Google Analytics or Plausible for visitor insights

🎯 Conclusion

You've now built a modern, professional portfolio with cutting-edge technologies! This setup gives you:

  • βœ… Lightning-fast development with Vite
  • βœ… Beautiful, responsive design with Tailwind CSS v4
  • βœ… Dark mode with system preference detection
  • βœ… Dynamic content with API integration
  • βœ… Clean, maintainable code structure

The best part? You can easily extend this foundation with additional features like a blog, case studies, or even an admin dashboard.

Ready to make it your own? Fork the code, customize the content, and deploy your portfolio today!


Found this guide helpful? Share it with your network! Have questions? Drop them in the comments below.


Tags: #React #TailwindCSS #Vite #WebDevelopment #Portfolio #Frontend #JavaScript #DevOps


This tutorial was originally published on Dev.to. Follow me for more web development content!

github: https://github.com/bkoimett/bkoimett-portofolio.git
linkedIn: https://www.linkedin.com/in/benjamin-koimett-699959366/

Top comments (0)