Let's build a lightning-fast, responsive portfolio grid for architecture studios that balances heavy visual assets with optimal browser performance.
What you'll learn:
- Structuring fluid CSS grid layouts for high-resolution imagery.
- Implementing client-side filtering without heavy layout shifts.
- Optimizing image loading for heavy visual portfolios.
- Managing accessible keyboard navigation for grid items.
Setting Up the Responsive Grid
Architecture portfolios live or die by their visual presentation. We need a CSS grid that scales smoothly from mobile screens up to ultrawide displays without breaking aspect ratios or causing content reflows.
import React from 'react';
type Project = {
id: string;
title: string;
category: 'residential' | 'commercial' | 'public';
image: string;
span: string;
};
const projects: Project[] = [
{ id: '1', title: 'The Glass Pavilion', category: 'residential', image: '/img/p1.jpg', span: 'col-span-1 md:col-span-2 row-span-2' },
{ id: '2', title: 'Urban Metro Hub', category: 'public', image: '/img/p2.jpg', span: 'col-span-1 row-span-1' },
{ id: '3', title: 'Helix Tower', category: 'commercial', image: '/img/p3.jpg', span: 'col-span-1 row-span-2' },
];
export function ProjectGrid() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 auto-rows-[300px] p-6 max-w-7xl mx-auto">
{projects.map((project) => (
<div
key={project.id}
className={`relative group overflow-hidden rounded-lg bg-neutral-900 ${project.span}`}
>
<img
src={project.image}
alt={project.title}
loading="lazy"
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-end p-6">
<span className="text-xs uppercase tracking-widest text-neutral-400">{project.category}</span>
<h3 className="text-xl font-medium text-white">{project.title}</h3>
</div>
</div>
))}
</div>
);
}
Speed Note: Always set explicit
auto-rowsor fixed aspect ratios on grid containers to prevent layout shifts while high-res images are downloading in the background.
Implementing Instant Category Filters
Architectural sites often feature dense project lists. Users want to filter by typology instantly without triggering full page reloads or jarring layout shifts.
import React, { useState, useMemo } from 'react';
export function FilterablePortfolio({ allProjects }: { allProjects: Project[] }) {
const [activeFilter, setActiveFilter] = useState<string>('all');
const filteredProjects = useMemo(() => {
if (activeFilter === 'all') return allProjects;
return allProjects.filter((p) => p.category === activeFilter);
}, [allProjects, activeFilter]);
const categories = ['all', 'residential', 'commercial', 'public'];
return (
<div>
<nav className="flex gap-4 justify-center py-8" aria-label="Project categories">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setActiveFilter(cat)}
className={`px-4 py-2 text-sm uppercase tracking-wider transition-colors ${
activeFilter === cat
? 'text-white border-b-2 border-white'
: 'text-neutral-400 hover:text-white'
}`}
>
{cat}
</button>
))}
</nav>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-6 max-w-7xl mx-auto">
{filteredProjects.map((project) => (
<div key={project.id} className="relative overflow-hidden rounded-lg bg-neutral-900 h-80">
<img src={project.image} alt={project.title} className="w-full h-full object-cover" />
<div className="absolute bottom-0 p-6 bg-gradient-to-t from-black/80 w-full">
<h3 className="text-lg font-medium text-white">{project.title}</h3>
</div>
</div>
))}
</div>
</div>
);
}
Gotcha: Keep state derived calculations inside
useMemowhen working with large arrays of project objects to avoid redundant filtering passes on unrelated re-renders.
Optimizing Image Loading & Compression
High-end architectural photography files are massive. Serving raw 4K PNGs will tank your Largest Contentful Paint (LCP). We must leverage modern formats and native lazy loading.
import React from 'react';
type OptimizedImageProps = {
src: string;
alt: string;
priority?: boolean;
};
export function OptimizedProjectImage({ src, alt, priority = false }: OptimizedImageProps) {
return (
<picture className="block w-full h-full overflow-hidden">
<source srcSet={`${src}.webp`} type="image/webp" />
<img
src={`${src}.jpg`}
alt={alt}
loading={priority ? 'eager' : 'lazy'}
fetchPriority={priority ? 'high' : 'auto'}
decoding="async"
className="w-full h-full object-cover"
/>
</picture>
);
}
Accessibility Tip: Never leave portfolio images without descriptive alt text. Screen reader users should understand what architectural style, material, or spatial layout is being displayed.
Building Smooth Modal Views for Project Details
When users click a project card, a full-screen modal should present detailed specs without forcing a navigation route change.
import React, { useEffect } from 'react';
type ModalProps = {
project: Project | null;
onClose: () => void;
};
export function ProjectModal({ project, onClose }: ModalProps) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
if (!project) return null;
return (
<div
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4 md:p-12"
role="dialog"
aria-modal="true"
>
<div className="relative max-w-5xl w-full bg-neutral-900 rounded-lg overflow-hidden flex flex-col md:flex-row">
<button
onClick={onClose}
className="absolute top-4 right-4 z-10 text-white bg-black/50 p-2 rounded-full hover:bg-black"
aria-label="Close modal"
>
✕
</button>
<div className="w-full md:w-2/3 h-96 md:h-auto">
<img src={project.image} alt={project.title} className="w-full h-full object-cover" />
</div>
<div className="w-full md:w-1/3 p-8 flex flex-col justify-between">
<div>
<span className="text-xs uppercase tracking-widest text-neutral-400">{project.category}</span>
<h2 className="text-2xl font-semibold text-white mt-2">{project.title}</h2>
<p className="text-neutral-400 mt-4 text-sm leading-relaxed">
Designed with sustainable materials and structural efficiency at the core of the spatial experience.
</p>
</div>
</div>
</div>
</div>
);
}
Summary
Building a stellar architectural portfolio app requires balancing heavy visual assets with snappy runtime performance. By combining CSS grid layouts, optimized WebP images, and lightweight client-side filtering, you give design work the presentation it deserves.
Next Steps:
- Add keyboard arrow navigation between project modal slides.
- Implement skeleton loaders for images loading over slow connections.
- Connect your portfolio grid to a headless CMS like Sanity or Strapi.