-
-
Notifications
You must be signed in to change notification settings - Fork 649
Description
Building a Beautiful Gallery for Magic Portfolio
So I wanted to add a photo gallery to my portfolio website, and let me tell you - it turned out way better than I expected!
Here's the story of how I built it, step by step.
What I Wanted to Achieve
I had a bunch of photos from my journey - campus life at IIT Patna, coding sessions, travel moments, creative projects. I wanted a clean, modern gallery that would:
- Show my photos in a beautiful masonry layout (like Pinterest)
- Be fast and responsive on mobile
- Have a cool modal popup for viewing full images
- Work smoothly with touch gestures
- Load images lazily for better performance
The File Structure
Here's the structure:
src/
├── app/gallery/
│ └── page.tsx # Main gallery page
├── components/gallery/
│ ├── MasonryGrid.tsx # The magic happens here
│ └── Gallery.module.scss # All the styling
└── resources/
└── content.js # Where I define my gallery data
Step 1: Setting Up the Gallery Data
First, I added my gallery configuration to content.js
. This is where I keep all my site's content organized:
const gallery = {
path: "/gallery",
label: "Gallery",
title: "Visual Stories",
subtitle: "Capturing moments and memories...",
description: "Photo gallery showcasing my journey...",
images: [
{
src: "/images/gallery/pastry-cafe-iit.jpg",
alt: "Pastry and coffee at IIT Patna cafe",
title: 'Pastry',
year: '2025'
},
// ... more images
]
}
Each image has:
src
: Path to the image filealt
: Description for accessibilitytitle
: A short title that shows in the modalyear
: When I took the photo
Step 2: Creating the Gallery Page
The gallery page (src/app/gallery/page.tsx
) is pretty straightforward.
- Sets up the metadata for SEO
- Renders a heading with title and subtitle
- Displays the
MasonryGrid
component
export default function Gallery() {
return (
<Column maxWidth="l">
<Heading marginBottom="xs" variant="display-strong-s">
{gallery.title}
</Heading>
<Heading marginBottom="l" variant="heading-default-xl" as="h2">
{gallery.subtitle}
</Heading>
<MasonryGrid />
</Column>
);
}
Simple, clean, does the job.
Step 3: The Heart - MasonryGrid Component
This is where the real magic happens. The MasonryGrid.tsx
component does a LOT:
Key Features I Built:
1. Masonry Layout
- Uses
react-masonry-css
for the Pinterest-style grid - Responsive columns: 3 on desktop, 2 on mobile
- Images arrange themselves naturally based on their aspect ratios
2. Smart Image Loading
- Lazy loading: Images only load when you scroll to them
- Automatically detects image dimensions and aspect ratios
- Handles loading states and errors gracefully
- Retry mechanism if an image fails to load
3. Image Classification
The component automatically figures out if each image is:
- Portrait (tall)
- Landscape (wide)
- Square
Then it sets the right aspect ratio for smooth loading btw I still feel that there is a room for lot of improvemnts and basically as of now it just works.
4. Shuffled Display
Every time you visit, the images are in a different order using the Fisher-Yates shuffle algorithm. Keeps things fresh! (just my thing ;) )
5. Modal Popup
Click any image and get a beautiful full-screen modal with:
- High-quality image display
- Navigation arrows (or really good swipe on mobile)
- Keyboard support (arrow keys, escape)
- Image title and year overlay below the image
6. Mobile-First Design
- Touch gestures: swipe left/right to navigate
- Pull down to close the image popup modal
- Optimized touch targets
- Smooth animations and feedback (I think so, can be improved)
The Code Structure
// Main component with state management
export default function MasonryGrid({ images, columns, loadingText }) {
// State for modal, loading, errors, etc.
const [selectedImage, setSelectedImage] = useState(null);
const [imageData, setImageData] = useState(new Map());
// Shuffle images on load
const shuffledImages = useMemo(() => shuffleArray(images), [images]);
// Individual image loading logic
const loadImageDimensions = useCallback(async (image) => {
// Load image, get dimensions, calculate aspect ratio
});
// Render the masonry grid
return (
<Masonry breakpointCols={breakpointColumnsObj}>
{shuffledImages.map((image, index) => (
<LazyImage
key={image.src}
image={image}
index={index}
onOpenModal={openModal}
// ... other props
/>
))}
</Masonry>
);
}
The Lazy Loading Component
function LazyImage({ image, index, onOpenModal }) {
const [isVisible, setIsVisible] = useState(false);
// Intersection Observer for lazy loading
useEffect(() => {
const observer = new IntersectionObserver(/* ... */);
// Only load when image enters viewport
}, []);
return (
<div onClick={() => onOpenModal(image, index)}>
{isVisible ? (
<Media src={image.src} alt={image.alt} />
) : (
<div>Loading placeholder</div>
)}
</div>
);
}
Step 4: Styling with SCSS
The Gallery.module.scss
file handles all the visual polish:
Grid Layout
.masonryGrid {
display: flex;
margin-left: calc(-1 * var(--static-space-16));
width: 100%;
}
.masonryGridColumn {
padding-left: var(--static-space-16);
background-clip: padding-box;
}
Modal Styling
.modal {
background: rgba(0, 0, 0, 0.95);
border: 1px solid var(--neutral-alpha-weak);
border-radius: var(--radius-m-static, 8px);
backdrop-filter: blur(20px);
}
Mobile Optimizations
.imageWrapper {
@media (max-width: 768px) {
min-height: 44px; // Touch target size
&:active {
opacity: 0.8;
transform: scale(0.98);
transition: all 0.1s ease;
}
}
}
Step 5: Touch Interactions
One of the coolest parts was adding mobile touch gestures:
Swipe Navigation
- Swipe left/right to go between images
- Smooth animations with proper thresholds
- Prevents accidental navigation on vertical scrolls
Pull-to-Close
- Drag down on the modal image to close it
- Visual feedback with scaling and opacity
- Snaps back if you don't drag far enough
Implementation
const handleTouchMove = (e) => {
const deltaX = touch.clientX - touchStart.x;
const deltaY = touch.clientY - touchStart.y;
// Pull-to-close: drag down to close
if (deltaY < -100 && Math.abs(deltaY) > Math.abs(deltaX)) {
closeModal();
}
// Horizontal swipe for navigation
if (Math.abs(deltaX) > 50) {
deltaX > 0 ? prevImage() : nextImage();
}
};
The Result
What I ended up with is a gallery that:
✅ Loads fast with lazy loading
✅ Looks great on all devices
✅ Has smooth animations and interactions
✅ Shows my photos in their best light
✅ Provides excellent user experience
✅ Is accessible with keyboard navigation
Performance Optimizations
Image Loading Strategy
- Only load images when they're about to be visible
- Use intersection observer with 100px margin
- Retry failed loads automatically
- Show loading states and error handling
Memory Management
- Clean up event listeners properly
- Use
useCallback
anduseMemo
to prevent unnecessary re-renders - Efficient state updates with Maps and Sets
Mobile Performance
- Touch-action optimizations
- Prevent scroll bounce on iOS
- Optimized transform animations
- Proper viewport handling
Future Improvements
here are some ideas for making it even better:
- Add image categories/filtering
- Implement infinite scroll for large galleries
- Add sharing functionality
- Include EXIF data display
- Add zoom functionality in modal
- Implement favorites/likes system
I might work on these in near future. (hope so)
and yes all the files are availabe in attached -> awesome-gallery.zip
The whole thing took me about a week to build and polish, but I'm really happy with how it turned out. It's one of those features that makes the whole site feel more personal and engaging.
If you're building something similar, my advice is: start simple, then add the fancy stuff. The core functionality (display images in a grid) is straightforward. The magic is in the details - smooth animations, proper loading states, and thoughtful mobile interactions. (I love to touch up things my way. Hope you like it and might find it helpful.)
That's the story of my gallery! From a folder full of photos to a polished, interactive experience that showcases my journey as a developer and creator.
Peace.