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
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(),
],
})
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');
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>
);
};
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;
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;
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;
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;
π 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');
});
β¨ Key Features Implemented
- Dark/Light Mode Toggle - Persistent theme switching with system preference detection
- Responsive Design - Mobile-first approach using Tailwind's responsive utilities
- Project Filtering - Category-based filtering with smooth transitions
- Loading States - Spinner indicators for async operations
- Backend Ready - Axios integration for API calls
- Smooth Animations - Hover effects and transitions
- Clean Navigation - React Router with active link highlighting
π Deployment
Build for Production
npm run build
Deploy to Vercel (Recommended)
npm install -g vercel
vercel
Deploy to Netlify
npm run build
# Drag and drop the 'dist' folder to Netlify
π SEO Optimization Tips
- 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">
- Use semantic HTML (header, main, section, article)
- Add alt text to all images
- 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
- Tailwind CSS v4 Documentation
- React Documentation
- Vite Documentation
- Unsplash - Free images for your projects
π‘ Pro Tips
- Performance: Use lazy loading for images and code splitting for routes
- Accessibility: Ensure proper contrast ratios and keyboard navigation
- Testing: Add unit tests for components using Jest and React Testing Library
- 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)