Let’s build a lightning-fast, accessible developer portfolio that highlights your projects without bloated UI libraries or sluggish animations.
What you'll learn:
- Structuring a modular portfolio project list in TypeScript
- Building a responsive grid layout with Tailwind CSS
- Optimizing images and loading states for a perfect 100 Lighthouse score
- Implementing keyboard navigation and ARIA attributes for accessibility
Defining Your Project Data Structure
Hardcoding HTML for every portfolio item gets messy fast. Start by defining a strict TypeScript interface for your projects so your data stays clean and predictable.
export interface Project {
id: string;
title: string;
description: string;
techStack: string[];
liveUrl: string;
githubUrl: string;
featured: boolean;
}
export const projects: Project[] = [
{
id: '1',
title: 'Real-Time Analytics Dashboard',
description: 'Tracks system metrics with WebSockets and Tailwind charts.',
techStack: ['React', 'TypeScript', 'Tailwind', 'WebSockets'],
liveUrl: 'https://example.com',
githubUrl: 'https://github.com/example/dashboard',
featured: true,
},
{
id: '2',
title: 'CLI Markdown Parser',
description: 'A zero-dependency parser built for speed in Node.js.',
techStack: ['TypeScript', 'Node.js', 'Jest'],
liveUrl: 'https://example.com',
githubUrl: 'https://github.com/example/parser',
featured: false,
},
];
Speed Note: Keep your project array in a separate data file or fetch it statically at build time. Avoid client-side fetching for static content like a portfolio to keep Time to First Byte (TTFB) minimal.
Creating the Responsive Grid Layout
A great portfolio adapts smoothly from mobile screens to ultra-wide monitors. Use CSS Grid with auto-fit columns to let Tailwind handle the layout math automatically.
import React from 'react';
import { projects, Project } from './data';
export function ProjectGrid() {
return (
<section className="max-w-6xl mx-auto px-4 py-12">
<h2 className="text-2xl font-bold tracking-tight text-slate-100 mb-8">
Featured Work
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
</section>
);
}
Gotcha: Avoid fixed pixel widths for grid containers. Using Tailwind's
grid-cols-1 md:grid-cols-2ensures cards wrap naturally without horizontal scrollbars on smaller viewports.
Crafting the Project Card Component
Each card needs clear visual hierarchy, distinct link targets for live demos versus source code, and clean typography that remains readable in both dark and light modes.
export function ProjectCard({ project }: { project: Project }) {
return (
<article className="flex flex-col justify-between bg-slate-900 border border-slate-800 rounded-lg p-6 hover:border-slate-700 transition-colors">
<div>
<h3 className="text-lg font-semibold text-slate-100 mb-2">
{project.title}
</h3>
<p className="text-slate-400 text-sm mb-4 leading-relaxed">
{project.description}
</p>
<ul className="flex flex-wrap gap-2 mb-6" aria-label="Technologies used">
{project.techStack.map((tech) => (
<li key={tech} className="bg-slate-800 text-slate-300 text-xs px-2.5 py-1 rounded-md">
{tech}
</li>
))}
</ul>
</div>
<div className="flex items-center gap-4 pt-4 border-t border-slate-800/60">
<a
href={project.liveUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-sky-400 hover:text-sky-300 transition-colors"
>
Live Demo →
</a>
<a
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-slate-400 hover:text-slate-200 transition-colors"
>
Source Code
</a>
</div>
</article>
);
}
Accessibility Tip: Always include
rel="noopener noreferrer"when usingtarget="_blank"to protect external links against tab-nabbing vulnerabilities.
Adding Keyboard Navigation and Focus States
Recruiters and fellow engineers often navigate portfolios using keyboards. Ensure interactive elements feature explicit focus rings so users always know their current position.
export function PortfolioButton({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a
href={href}
className="inline-flex items-center justify-center px-4 py-2 rounded-md font-medium text-white bg-sky-600 hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-offset-2 focus:ring-offset-slate-950 transition-all"
>
{children}
</a>
);
}
Gotcha: Never remove default focus outlines using
outline-noneunless you provide a high-contrast custom focus ring like thefocus:ring-2utility shown above.
Summary
Building a custom developer portfolio gives you complete control over performance, accessibility, and design without wrestling with heavy template engines. By pairing TypeScript interfaces with Tailwind CSS grids, you get a maintainable codebase that loads instantly.
Next Steps:
- Add a dark/light theme toggle using Tailwind's
classdark mode strategy. - Integrate MDX to write detailed case studies for each featured project.
- Run a Lighthouse audit to verify your performance and accessibility scores.