Responsive Waterfall/Masonry Photo Grid in React

Elgun Mehdiyev's profile picture

Elgun Mehdiyev

June 30, 2024 5 min read

  • Github Dark logoGithub White logo
  • LinkedIn Dark logoLinkedIn White logo
  • X Dark logoX White logo
  • Threads Dark logoThreads White logo
Waterfall/Masonry grid style photo galleryWaterfall/Masonry grid style photo gallery

In my project, I implemented a responsive photo grid that maintains the original aspect ratios of photos while displaying them in a masonry/waterfall layout. This ensures that the photos retain their order and are displayed consistently across different screen sizes.

Approach

To achieve this, I used the following approach:

1. Creating the useMediaQuery Hook

First, I created a custom useMediaQuery hook to get the current screen width. This hook is essential for determining the number of columns in the grid and keeping the order of photos. Here's the implementation:

Typescript

// src/hooks/useMediaQuery.ts
        
'use client';

import { useEffect, useState } from 'react';

export function useMediaQuery(query: string) {
  const [value, setValue] = useState(false);

  useEffect(() => {
    function onChange(event: MediaQueryListEvent) {
      setValue(event.matches);
    }

    const result = matchMedia(query);
    result.addEventListener('change', onChange);
    setValue(result.matches);

    return () => result.removeEventListener('change', onChange);
  }, [query]);

  return value;
}

2. Using useMediaQuery to Determine Columns

Next, I used the useMediaQuery hook to determine the screen width and adjust the number of columns accordingly. The grid adapts to different screen sizes, displaying up to 4 columns based on the viewport width. This can also be achieved using Tailwind's responsive utility variants. However, knowing the number of columns is crucial to display the photos in the correct order.

Typescript

// src/components/ui/PhotoGrid/PhotoGrid.tsx

'use client';

import { PhotoCard } from './components';

import { Photo } from '@/types/Images';
import { useMediaQuery } from '@/hooks/useMediaQuery';

type PhotoGridProps = {
  photos: Array<Photo>;
  base64results: Array<string>;
};
        
function PhotoGrid({ photos, base64results }: PhotoGridProps) {
  const numberOfPhotos = photos.length;
  const isXLarge = useMediaQuery('(min-width: 1448px)');
  const isLarge = useMediaQuery('(min-width: 1096px');
  const isMedium = useMediaQuery('(min-width: 722px)');
  const numberOfCols = isXLarge ? 4 : isLarge ? 3 : isMedium ? 2 : 1;

  return (
    <ul className={`grid h-auto w-full gap-x-4 ${isXLarge ? 'grid-cols-4'
    : isLarge ? 'grid-cols-3'
    : isMedium ? 'grid-cols-2'
    : 'grid-cols-1'}`}>
      // Columns will go here
      ...
    </ul>
  );
}

export default PhotoGrid;

3. Distributing Photos in Columns

To maintain the order of photos regardless of the screen width, each column displays every nth photo in the photos array (where n is the number of columns). Here's the implementation:

Typescript

// src/components/ui/PhotoGrid/PhotoGrid.tsx

{Array.from({ length: numberOfCols }).map((_, colIndex) => (
  <div key={colIndex} className='flex h-auto w-full flex-col gap-y-4'>
    {photos.map((_, photoIndex) => {
      const index = colIndex + numberOfCols * photoIndex;
      return index < numberOfPhotos ? (
        <li key={photos[index].id}>
          <PhotoCard
            photo={photos[index]}
            base64={base64results[index]}
          />
        </li>
      ) : null;
    })}
  </div>
))}

Result

This way, photos are evenly distributed across the columns while retaining their original aspect ratios. By iterating over the photos and placing them in the appropriate column, the grid maintains order and visual consistency. The implementation ensures a masonry or waterfall effect, where photos stack vertically in columns, creating a visually appealing and organized display.