Building a Modern Photo Gallery Website with React and Tailwind CSS

OdynDevOdynDev
12 min read

In this tutorial, I'll guide you through creating a beautiful, responsive photo gallery website using React and Tailwind CSS. We'll break down the components and styling techniques used to create an engaging user experience with smooth animations and a professional design.


Introduction

Modern websites need to be visually appealing, responsive, and performant. In this tutorial, we'll examine a photo gallery website that showcases various images in different categories. The site features:

  • A gradient hero section with decorative elements

  • Category filtering

  • Interactive gallery navigator

  • Newsletter subscription section

  • Responsive footer

Let's dive into the implementation details.


Setting Up the Main Page Structure

The main page component (HomePage) serves as the container for our entire gallery website. Let's analyze its structure:

// src/app/page.jsx
import InteractiveGalleryNavigator from '@/components/InteractiveGalleryNavigator';

export default function HomePage() {
  const galleryItems = [
    { id: 1, title: "Sunrise Over The Mountains", image: "Sunrise Mountains.jpg", category: "Nature" },
    { id: 2, title: "Urban Architecture", image: "Urban Architecture.jpg", category: "City" },
    // Additional gallery items...
  ];

  return (
    <main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100">
      {/* Hero Section */}
      {/* Category Section */}
      {/* Gallery Section */}
      {/* Newsletter Section */}
      {/* Footer */}
    </main>
  );
}

The page is structured in distinct sections, each with a specific purpose and design. Let's examine each section in detail.


Creating a Stunning Hero Section

The hero section is the first thing visitors see, so it needs to make an impact:

<section className="relative bg-gradient-to-r from-purple-600 to-blue-500 text-white overflow-hidden">
  {/* Decorative background elements */}
  <div className="absolute inset-0 overflow-hidden">
    <div className="absolute -top-24 -right-24 w-96 h-96 bg-purple-400 opacity-20 rounded-full blur-3xl"></div>
    <div className="absolute top-32 -left-24 w-72 h-72 bg-blue-400 opacity-20 rounded-full blur-3xl"></div>
    <div className="absolute -bottom-24 right-48 w-80 h-80 bg-indigo-400 opacity-20 rounded-full blur-3xl"></div>
  </div>

  {/* Content container */}
  <div className="container mx-auto py-24 px-6 relative z-10">
    <div className="max-w-3xl mx-auto text-center">
      <h1 className="text-5xl font-bold mb-6 leading-tight">
        Discover the world of amazing images from <span className="text-transparent bg-clip-text bg-gradient-to-r from-yellow-200 to-yellow-400">Odyn</span>Dev.
      </h1>
      <p className="text-xl mb-10 text-blue-50 leading-relaxed">
        My interactive gallery showcases unique images from around the world.
        Dive into the world of art and discover beauty in every frame.
      </p>
      {/* CTA buttons */}
      <div className="flex flex-col sm:flex-row justify-center gap-4">
        <button className="px-8 py-3 bg-white text-purple-600 rounded-lg font-medium hover:bg-blue-50 transition-colors shadow-lg hover:shadow-xl transform hover:-translate-y-1 duration-300">
          Browse the gallery
        </button>
        <button className="px-8 py-3 bg-transparent border-2 border-white text-white rounded-lg font-medium hover:bg-white/10 transition-colors">
          Learn more
        </button>
      </div>
    </div>
  </div>

  {/* Wave separator */}
  <div className="absolute bottom-0 left-0 w-full overflow-hidden leading-none">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 120" preserveAspectRatio="none" className="w-full h-16 text-gray-50">
      <path d="M321.39,56.44c58-10.79,114.16-30.13,172-41.86,82.39-16.72,168.19-17.73,250.45-.39C823.78,31,906.67,72,985.66,92.83c70.05,18.48,146.53,26.09,214.34,3V0H0V27.35A600.21,600.21,0,0,0,321.39,56.44Z" fill="currentColor"></path>
    </svg>
  </div>
</section>

Key Design Elements:

  1. Gradient Background: Using bg-gradient-to-r from-purple-600 to-blue-500 creates a smooth color transition.

  2. Decorative Elements: Blurred circles with low opacity add depth and visual interest.

  3. Text Styling: The brand name "Odyn" uses a text gradient with text-transparent bg-clip-text.

  4. Responsive Button Layout: Buttons stack on small screens and align horizontally on larger screens.

  5. Animated Hover Effects: The primary button includes shadow, translation, and color transitions on hover.

  6. Wave Separator: An SVG wave creates a smooth transition to the next section.


Building the Category Section

The category section helps users navigate the gallery by content type:

<section className="py-16 px-6">
  <div className="container mx-auto mb-16">
    <div className="text-center mb-12">
      <h2 className="text-3xl font-bold text-gray-800 mb-4">Popular categories</h2>
      <p className="text-gray-600 max-w-2xl mx-auto">Discover a variety of photo categories and find inspiration for your next project.</p>
    </div>

    <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
      {['Nature', 'City', 'People', 'Art', 'Tech', 'Architecture'].map((category) => (
        <div key={category} className="bg-white rounded-xl shadow-md p-6 flex flex-col items-center text-center transform hover:scale-105 transition-all duration-300">
          <div className="w-12 h-12 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center mb-4">
            <span className="font-bold">{category.charAt(0)}</span>
          </div>
          <h3 className="font-medium text-gray-800">{category}</h3>
          <p className="text-sm text-gray-500 mt-2">{Math.floor(Math.random() * 100)} photos</p>
        </div>
      ))}
    </div>
  </div>
</section>

Key Design Elements:

  1. Responsive Grid: Using Tailwind's grid system with responsive breakpoints (grid-cols-2 md:grid-cols-3 lg:grid-cols-6).

  2. Card Hover Effect: Cards scale up slightly on hover with hover:scale-105.

  3. Visual Indicators: Each category has a circular badge with the first letter.

  4. Clean Typography: Clear hierarchy with differentiated text sizes and colors.


The gallery section is where we display our photo collection:

<section className="py-16 px-6 bg-white">
  <div className="container mx-auto">
    <div className="text-center mb-16">
      <h2 className="text-3xl font-bold text-gray-800 mb-4">My collection</h2>
      <div className="w-24 h-1 bg-gradient-to-r from-purple-600 to-blue-500 mx-auto mb-4"></div>
      <p className="text-gray-600 max-w-2xl mx-auto">
        Immerse yourself in the world of visual art. Our photos are carefully curated to inspire and amaze.
      </p>
    </div>

    <InteractiveGalleryNavigator items={galleryItems} />
  </div>
</section>

This section uses a custom component called InteractiveGalleryNavigator that receives the gallery items as props. The component would handle the display and interaction with the photo collection.

Key Design Elements:

  1. Section Title with Decorative Line: A small gradient line emphasizes the section title.

  2. Custom Component: Separating the gallery functionality into its own component promotes reusability and maintainability.


One of the most eye-catching features of our gallery is the interactive 3D effect applied to each image card. This effect creates an immersive experience by making cards respond to the user's mouse movements.

How the 3D Effect Works

The effect uses several techniques to create a realistic 3D experience:

  1. 3D Rotation: Cards rotate based on mouse position relative to the card's center

  2. Perspective Transform: Adds depth to the rotation effect

  3. Dynamic Light Effect: A subtle highlight follows the cursor position

  4. Animated Content: Text and buttons animate on hover with staggered timing

Here's how we implement it in our GalleryCard component:

function GalleryCard({ item }) {
  const cardRef = useRef(null);
  const [rotation, setRotation] = useState({ x: 0, y: 0 });
  const [isHovered, setIsHovered] = useState(false);
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (e) => {
    if (!cardRef.current) return;

    const rect = cardRef.current.getBoundingClientRect();
    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;

    // Calculate distance from center as percentage
    const x = (e.clientX - centerX) / (rect.width / 2);
    const y = (e.clientY - centerY) / (rect.height / 2);

    // Limit rotation to 8 degrees
    const maxRotation = 8;
    const rotX = -y * maxRotation; // Inverted for natural movement
    const rotY = x * maxRotation;

    setRotation({ x: rotX, y: rotY });
    setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
  };

  const resetCard = () => {
    setRotation({ x: 0, y: 0 });
    setIsHovered(false);
  };

  return (
    <div
      ref={cardRef}
      className="group relative overflow-hidden rounded-xl shadow-lg transition-all duration-300 h-80 cursor-pointer"
      onMouseMove={handleMouseMove}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={resetCard}
      style={{
        transform: isHovered 
          ? `perspective(1000px) rotateX(${rotation.x}deg) rotateY(${rotation.y}deg) scale3d(1.05, 1.05, 1.05)` 
          : 'perspective(1000px) rotateX(0) rotateY(0) scale3d(1, 1, 1)',
        transition: 'transform 0.2s ease-out',
      }}
    >
      {/* Main image */}
      <div className="absolute inset-0 w-full h-full">
        <img
          src={item.image}
          alt={item.title}
          className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
        />
      </div>

      {/* Light effect on hover */}
      {isHovered && (
        <div 
          className="absolute w-40 h-40 rounded-full bg-white opacity-30 blur-xl pointer-events-none"
          style={{
            left: `${position.x - 80}px`,
            top: `${position.y - 80}px`,
            transition: 'opacity 0.2s ease-out',
          }}
        />
      )}

      {/* Text overlay */}
      <div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent flex flex-col justify-end p-6 transition-opacity duration-300">
        <div 
          className="transform transition-transform duration-500"
          style={{
            transform: isHovered ? 'translateY(0)' : 'translateY(20px)',
          }}
        >
          <span className="inline-block px-3 py-1 bg-purple-600 text-white text-xs font-semibold rounded-full mb-3 shadow-md">
            {item.category}
          </span>
          <h3 className="text-white text-xl font-bold mb-3">{item.title}</h3>

          {isHovered && (
            <div className="mt-4 opacity-0 animate-fadeIn" style={{ animationDelay: '0.1s', animationFillMode: 'forwards' }}>
              <Link href={`/gallery/${item.id}`}>
                <button className="px-5 py-2.5 bg-white text-purple-600 rounded-lg font-medium hover:bg-purple-50 transition-colors shadow-md transform hover:-translate-y-1 hover:shadow-lg duration-300">
                  See more
                </button>
              </Link>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

Key Implementation Details

  1. Mouse Position Calculation: We calculate the mouse position relative to the card's center and convert it to rotation angles.

  2. Dynamic Transform Application: The rotation is applied using CSS transforms with the perspective property to create depth.

  3. Light Effect Following Cursor: A semi-transparent, blurred white circle follows the cursor position to create a highlight effect.

  4. Staggered Content Animation: Text slides up from a slightly lower position, and the button fades in with a slight delay.

This 3D effect creates a premium, interactive experience that makes the gallery feel more immersive and engaging for users. It's particularly effective because it combines multiple subtle animations that work together to create a cohesive effect.


Creating the Newsletter Section

Encouraging user engagement with a newsletter signup:

<section className="py-20 px-6 bg-gray-50">
  <div className="container mx-auto max-w-4xl bg-gradient-to-r from-blue-500 to-purple-600 rounded-2xl shadow-xl overflow-hidden">
    <div className="flex flex-col md:flex-row items-center">
      <div className="p-10 md:p-12 flex-1 text-white">
        <h3 className="text-2xl font-bold mb-4">Join our newsletter</h3>
        <p className="text-blue-100 mb-6">Receive notifications about new photos and events. No spam, we promise!</p>
        <div className="flex flex-col sm:flex-row gap-3">
          <input 
            type="email" 
            placeholder="Your e-mail address" 
            className="px-4 py-3 rounded-lg text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-300 flex-1"
          />
          <button className="px-6 py-3 bg-white text-purple-600 rounded-lg font-medium hover:bg-blue-50 transition-colors whitespace-nowrap">
            Sign up
          </button>
        </div>
      </div>
      <div className="hidden md:block w-64 h-64 relative">
        <div className="absolute inset-0 bg-white opacity-20 rounded-full transform translate-x-1/3"></div>
      </div>
    </div>
  </div>
</section>

Key Design Elements:

  1. Contained Card Design: The newsletter form sits in a rounded card with a gradient background.

  2. Decorative Element: A semi-transparent circle adds visual interest on larger screens.

  3. Focus States: The input field has a distinct focus style with focus:ring-2.

  4. Responsive Layout: The form stacks vertically on small screens and aligns horizontally on larger screens.


A comprehensive footer with multiple columns of links:

<footer className="bg-gray-900 text-gray-400 py-12 px-6">
  <div className="container mx-auto">
    <div className="flex flex-col md:flex-row justify-between gap-8">
      <div className="md:w-1/3">
        <h3 className="text-2xl font-bold text-white mb-4">Gallery of Art by OdynDev.</h3>
        <p className="mb-4">We discover beauty in every frame. Our mission is to inspire through visual art.</p>
        <div className="flex gap-4">
          {/* Social media icons */}
        </div>
      </div>
      <div className="grid grid-cols-2 md:grid-cols-3 gap-8">
        {/* Footer navigation links */}
      </div>
    </div>
    <div className="mt-12 pt-8 border-t border-gray-800 text-center text-sm">
      <p>&copy; Gallery by OdynDev. All rights reserved.</p>
    </div>
  </div>
</footer>

Key Design Elements:

  1. Dark Background: Using bg-gray-900 for contrast with the rest of the page.

  2. Multi-Column Layout: Using flexbox and grid for responsive column arrangements.

  3. Social Media Links: Including SVG icons for social media platforms.

  4. Hover Effects: Links brighten on hover with hover:text-white.

  5. Separated Copyright: A border separates the copyright notice from the main footer content.


Styling with globals.css

Our global CSS file includes some basic theme configuration:

@import "tailwindcss";

:root {
  --background: #ffffff;
  --foreground: #171717;
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

body {
  background: var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.animate-fadeIn {
  animation: fadeIn 0.5s ease-in-out;
}

Key Styling Features:

  1. CSS Variables: Using custom properties for theme colors.

  2. Dark Mode Support: Adjusting colors based on user preference with prefers-color-scheme.

  3. Animation Definition: Creating a custom fade-in animation for use throughout the site.

Implementing the InteractiveGalleryNavigator Component

While the provided code doesn't include the implementation of the InteractiveGalleryNavigator component, we can infer that it should:

  1. Display gallery items in a grid or other visually appealing layout

  2. Allow filtering by category

  3. Handle interactions like image viewing or enlargement

  4. Implement animations for smooth transitions


Here's a suggested implementation:

// src/components/InteractiveGalleryNavigator.jsx
import { useState } from 'react';

export default function InteractiveGalleryNavigator({ items }) {
  const [activeCategory, setActiveCategory] = useState('All');
  const [selectedItem, setSelectedItem] = useState(null);

  const categories = ['All', ...new Set(items.map(item => item.category))];

  const filteredItems = activeCategory === 'All' 
    ? items 
    : items.filter(item => item.category === activeCategory);

  return (
    <div className="space-y-8">
      {/* Category filters */}
      <div className="flex flex-wrap justify-center gap-2 mb-8">
        {categories.map(category => (
          <button
            key={category}
            onClick={() => setActiveCategory(category)}
            className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
              activeCategory === category
                ? 'bg-purple-600 text-white'
                : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
            }`}
          >
            {category}
          </button>
        ))}
      </div>

      {/* Gallery grid */}
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
        {filteredItems.map(item => (
          <div
            key={item.id}
            onClick={() => setSelectedItem(item)}
            className="group relative overflow-hidden rounded-xl shadow-md cursor-pointer transform transition-transform hover:scale-[1.02] duration-300"
          >
            <img
              src={`/images/${item.image}`}
              alt={item.title}
              className="w-full h-64 object-cover"
            />
            <div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end p-4">
              <div className="text-white">
                <h3 className="font-medium text-lg">{item.title}</h3>
                <p className="text-sm text-gray-200">{item.category}</p>
              </div>
            </div>
          </div>
        ))}
      </div>

      {/* Modal for selected item */}
      {selectedItem && (
        <div className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4" onClick={() => setSelectedItem(null)}>
          <div className="max-w-4xl max-h-[90vh] overflow-hidden rounded-xl animate-fadeIn" onClick={e => e.stopPropagation()}>
            <img
              src={`/images/${selectedItem.image}`}
              alt={selectedItem.title}
              className="w-full h-auto max-h-[80vh] object-contain"
            />
            <div className="bg-white p-4">
              <h3 className="font-bold text-xl text-gray-800">{selectedItem.title}</h3>
              <p className="text-gray-600">{selectedItem.category}</p>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Implementing the InteractiveGalleryNavigator Component

The complete implementation of our InteractiveGalleryNavigator includes both the filtering functionality and the interactive card component:

"use client";

import { useState, useRef } from 'react';
import Link from 'next/link';

export default function InteractiveGalleryNavigator({ items }) {
  if (!items || !items.length) {
    items = [
      { id: 1, title: "Naturalny krajobraz", image: "/api/placeholder/800/500", category: "Natura" },
      { id: 2, title: "Urban architecture", image: "Urban Architecture.jpg", category: "Miasto" },
      { id: 3, title: "Portret artystyczny", image: "People.jpg", category: "Ludzie" },
      { id: 4, title: "Abstrakcja", image: "/api/placeholder/800/500", category: "Sztuka" },
      { id: 5, title: "Technologia przyszłości", image: "/api/placeholder/800/500", category: "Tech" },
    ];
  }

  const [activeFilter, setActiveFilter] = useState('All');
  const categories = ['All', ...Array.from(new Set(items.map(item => item.category)))];

  const filteredItems = activeFilter === 'All' 
    ? items 
    : items.filter(item => item.category === activeFilter);

  return (
    <div className="w-full mx-auto">
      {/* Category filters */}
      <div className="flex flex-wrap justify-center gap-2 mb-10">
        {categories.map((category) => (
          <button
            key={category}
            onClick={() => setActiveFilter(category)}
            className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-300 ${
              activeFilter === category
                ? 'bg-purple-600 text-white shadow-lg scale-105'
                : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
            }`}
          >
            {category}
          </button>
        ))}
      </div>

      {/* Gallery */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
        {filteredItems.map((item) => (
          <GalleryCard key={item.id} item={item} />
        ))}
      </div>
    </div>
  );
}

Conclusion

In this tutorial, we've examined how to build a modern photo gallery website using React and Tailwind CSS. The design incorporates:

  • Modern UI Elements: Gradients, shadows, and animations that create visual appeal

  • Responsive Design: Layouts that adapt gracefully to different screen sizes

  • Interactive Components: Category filters and gallery navigation

  • 3D Interactive Effects: Creating an immersive experience with mouse-following animations

  • Performance Considerations: Using CSS transitions for smooth animations

  • Maintainable Structure: Breaking the UI into logical sections and components

By applying these techniques, you can create professional-looking websites that not only showcase content effectively but also provide an engaging user experience.

This approach demonstrates how powerful the combination of React and Tailwind CSS can be for creating sophisticated, responsive web applications with minimal custom CSS.


Thanks so much for visiting! 🙌

I’m grateful for your time and attention.
If you’re interested in seeing more of what I do, feel free to check out my portfolio:
👉 [Click here]

I’m always striving to grow, learn new things, and create even more exciting projects. 🚀
You can also explore more of my work and thoughts right here on my Profile. Have fun browsing!

10
Subscribe to my newsletter

Read articles from OdynDev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

OdynDev
OdynDev

My name is Daniel. I code, design, and get things done. I'm not a fan of school, but I love learning - on my own terms. I build websites that make sense, look good, and never feel boring. If you're looking for someone who does things right, with clarity and a human touch - I’m your guy.