We use cookies to enhance your experience on the site
CodeWorlds

Grid Layouts and Modals - Instagram UI

Time to create a gallery interface inspired by Instagram. You'll learn to create grid layouts and modals.

Grid Layout - Photo Grid

Tailwind CSS has great classes for grid layouts.

Basic 3x3 grid:

1<div className="grid grid-cols-3 gap-4">
2  <div className="bg-gray-200 aspect-square">Photo 1</div>
3  <div className="bg-gray-200 aspect-square">Photo 2</div>
4  <div className="bg-gray-200 aspect-square">Photo 3</div>
5  <div className="bg-gray-200 aspect-square">Photo 4</div>
6  <div className="bg-gray-200 aspect-square">Photo 5</div>
7  <div className="bg-gray-200 aspect-square">Photo 6</div>
8</div>

Classes:

  • grid
    - activates CSS Grid
  • grid-cols-3
    - 3 columns
  • gap-4
    - 16px gap between elements
  • aspect-square
    - square shape (1:1)

Responsive grid:

1<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-1">
2  {/* Photos */}
3</div>

Behavior:

  • Mobile: 1 column
  • Small (≥640px): 2 columns
  • Medium (≥768px): 3 columns
  • Large (≥1024px): 4 columns

Instagram Gallery Grid

1import Image from 'next/image';
2
3interface Photo {
4  id: number;
5  url: string;
6  caption: string;
7  likes: number;
8}
9
10export default function TreasureGallery({ photos }: { photos: Photo[] }) {
11  return (
12    <div className="max-w-5xl mx-auto p-4">
13      <h1 className="text-2xl font-bold mb-6">Treasure Gallery</h1>
14
15      <div className="grid grid-cols-3 gap-1">
16        {photos.map((photo) => (
17          <div
18            key={photo.id}
19            className="relative aspect-square group cursor-pointer overflow-hidden"
20          >
21            <Image
22              src={photo.url}
23              alt={photo.caption}
24              fill
25              className="object-cover transition-transform group-hover:scale-110"
26            />
27
28            {/* Overlay on hover */}
29            <div className="absolute inset-0 bg-black bg-opacity-60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
30              <div className="text-white text-center space-y-2">
31                <p className="text-2xl font-bold">❤️ {photo.likes}</p>
32                <p className="text-sm px-4">{photo.caption}</p>
33              </div>
34            </div>
35          </div>
36        ))}
37      </div>
38    </div>
39  );
40}

Modal Dialog - Enlarged Photo

A modal is a window that appears on top of the page. We'll use it to show the full photo.

Simple Modal with useState:

1'use client';
2
3import { useState } from 'react';
4import Image from 'next/image';
5
6interface Photo {
7  id: number;
8  url: string;
9  caption: string;
10  author: string;
11  likes: number;
12}
13
14export default function Gallery({ photos }: { photos: Photo[] }) {
15  const [selectedPhoto, setSelectedPhoto] = useState<Photo | null>(null);
16
17  return (
18    <>
19      {/* Grid */}
20      <div className="grid grid-cols-3 gap-1">
21        {photos.map((photo) => (
22          <div
23            key={photo.id}
24            onClick={() => setSelectedPhoto(photo)}
25            className="relative aspect-square cursor-pointer"
26          >
27            <Image src={photo.url} alt={photo.caption} fill className="object-cover" />
28          </div>
29        ))}
30      </div>
31
32      {/* Modal */}
33      {selectedPhoto && (
34        <div
35          className="fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-4"
36          onClick={() => setSelectedPhoto(null)}
37        >
38          <div
39            className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-hidden"
40            onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside
41          >
42            {/* Header */}
43            <div className="flex items-center justify-between p-4 border-b">
44              <div className="flex items-center gap-3">
45                <div className="text-2xl">🏴‍☠️</div>
46                <h3 className="font-bold">{selectedPhoto.author}</h3>
47              </div>
48              <button
49                onClick={() => setSelectedPhoto(null)}
50                className="text-gray-500 hover:text-gray-700 text-2xl"
51              >
5253              </button>
54            </div>
55
56            {/* Image */}
57            <div className="relative aspect-square">
58              <Image
59                src={selectedPhoto.url}
60                alt={selectedPhoto.caption}
61                fill
62                className="object-contain"
63              />
64            </div>
65
66            {/* Footer */}
67            <div className="p-4 border-t">
68              <div className="flex items-center gap-4 mb-2">
69                <button className="text-2xl hover:scale-110 transition-transform">
70                  ❤️
71                </button>
72                <button className="text-2xl hover:scale-110 transition-transform">
73                  💬
74                </button>
75                <button className="text-2xl hover:scale-110 transition-transform">
76                  📤
77                </button>
78              </div>
79              <p className="font-bold mb-1">{selectedPhoto.likes} likes</p>
80              <p className="text-gray-800">
81                <span className="font-bold">{selectedPhoto.author}</span>{' '}
82                {selectedPhoto.caption}
83              </p>
84            </div>
85          </div>
86        </div>
87      )}
88    </>
89  );
90}

Portal Pattern - Better Modals

For production use libraries like Radix UI or Headless UI, but here's a simple portal:

1'use client';
2
3import { useEffect, useState } from 'react';
4import { createPortal } from 'react-dom';
5
6function Modal({
7  isOpen,
8  onClose,
9  children
10}: {
11  isOpen: boolean;
12  onClose: () => void;
13  children: React.ReactNode;
14}) {
15  const [mounted, setMounted] = useState(false);
16
17  useEffect(() => {
18    setMounted(true);
19    return () => setMounted(false);
20  }, []);
21
22  useEffect(() => {
23    if (isOpen) {
24      document.body.style.overflow = 'hidden'; // Block scroll
25    } else {
26      document.body.style.overflow = 'unset';
27    }
28  }, [isOpen]);
29
30  if (!mounted || !isOpen) return null;
31
32  return createPortal(
33    <div
34      className="fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-4"
35      onClick={onClose}
36    >
37      <div onClick={(e) => e.stopPropagation()}>
38        {children}
39      </div>
40    </div>,
41    document.body
42  );
43}
44
45export default Modal;

Keyboard Navigation - Escape to close

Add keyboard support:

1'use client';
2
3import { useEffect } from 'react';
4
5function useKeyPress(key: string, callback: () => void) {
6  useEffect(() => {
7    const handler = (e: KeyboardEvent) => {
8      if (e.key === key) {
9        callback();
10      }
11    };
12
13    window.addEventListener('keydown', handler);
14    return () => window.removeEventListener('keydown', handler);
15  }, [key, callback]);
16}
17
18function Modal({ isOpen, onClose, children }: ModalProps) {
19  useKeyPress('Escape', onClose);
20
21  // Rest of the code...
22}

Image Carousel - Switching Between Photos

Add arrows to switch between photos:

1'use client';
2
3import { useState } from 'react';
4
5export default function Gallery({ photos }: { photos: Photo[] }) {
6  const [currentIndex, setCurrentIndex] = useState(0);
7  const [isModalOpen, setIsModalOpen] = useState(false);
8
9  const nextPhoto = () => {
10    setCurrentIndex((prev) => (prev + 1) % photos.length);
11  };
12
13  const prevPhoto = () => {
14    setCurrentIndex((prev) => (prev - 1 + photos.length) % photos.length);
15  };
16
17  const currentPhoto = photos[currentIndex];
18
19  return (
20    <>
21      {/* Grid - clicking opens modal with given index */}
22      <div className="grid grid-cols-3 gap-1">
23        {photos.map((photo, index) => (
24          <div
25            key={photo.id}
26            onClick={() => {
27              setCurrentIndex(index);
28              setIsModalOpen(true);
29            }}
30            className="relative aspect-square cursor-pointer"
31          >
32            <Image src={photo.url} alt={photo.caption} fill className="object-cover" />
33          </div>
34        ))}
35      </div>
36
37      {/* Modal with carousel */}
38      {isModalOpen && (
39        <div className="fixed inset-0 bg-black bg-opacity-95 z-50 flex items-center justify-center">
40          <button
41            onClick={prevPhoto}
42            className="absolute left-4 text-white text-4xl hover:scale-110 transition-transform"
43          >
4445          </button>
46
47          <div className="max-w-4xl w-full">
48            <div className="relative aspect-square">
49              <Image
50                src={currentPhoto.url}
51                alt={currentPhoto.caption}
52                fill
53                className="object-contain"
54              />
55            </div>
56            <p className="text-white text-center mt-4">{currentPhoto.caption}</p>
57          </div>
58
59          <button
60            onClick={nextPhoto}
61            className="absolute right-4 text-white text-4xl hover:scale-110 transition-transform"
62          >
6364          </button>
65
66          <button
67            onClick={() => setIsModalOpen(false)}
68            className="absolute top-4 right-4 text-white text-3xl"
69          >
7071          </button>
72        </div>
73      )}
74    </>
75  );
76}

Masonry Layout - Pinterest style

For a more advanced layout (varying heights):

1<div className="columns-1 sm:columns-2 md:columns-3 lg:columns-4 gap-4 space-y-4">
2  {photos.map((photo) => (
3    <div key={photo.id} className="break-inside-avoid">
4      <Image
5        src={photo.url}
6        alt={photo.caption}
7        width={300}
8        height={Math.random() * 200 + 200} // Varying heights
9        className="w-full rounded-lg"
10      />
11    </div>
12  ))}
13</div>

Summary 🎓

Grid Layout -

grid grid-cols-3 gap-4
Responsive grid -
grid-cols-1 md:grid-cols-3
aspect-square - square elements ✅ Modal - overlay with fixed position ✅ e.stopPropagation() - prevent closing modal on inside click ✅ Keyboard handling - Escape to close ✅ Image carousel - prev/next navigation ✅ Hover effects - group-hover:opacity-100 ✅ Masonry layout - columns-* for Pinterest style

In the next exercise we'll add photo uploading and comments!

See you there! 🚀

Go to CodeWorlds