LearnAI ToolsCareerPractice BuildsPlayContact
HTMLBeginner~1 hour

Image Gallery Page

Build a responsive-ready gallery layout with images, captions, and a lightbox link structure.

ImagesFigure/FigcaptionLinks

Overview

An image gallery looks simple — a grid of pictures — but it's a great project for two HTML features that don't come up until you need them: `<figure>`/`<figcaption>` for pairing an image with a real caption, not just a paragraph sitting near it, and fragment-identifier links, which can build a working "lightbox" that enlarges an image on click without a single line of JavaScript.

This tutorial builds a six-photo gallery where every thumbnail links to a full-size overlay view, using nothing but anchor tags, ids, and a small CSS rule that shows or hides content based on the URL's fragment (the part after `#`). It is a real, working feature, not a simplified stand-in for one.

What You'll Build
  • A gallery grid of six images, each wrapped in a `<figure>` with a real `<figcaption>`.
  • A lightbox link structure — clicking a thumbnail opens a full-size overlay.
  • A "close" link inside the overlay that returns to the plain gallery view.
  • Meaningful `alt` text on every image, distinct from its caption.
  • A layout that reflows into fewer columns as the viewport narrows.

Prerequisites

  • The `<img>` element and its `src`/`alt`/`width`/`height` attributes.
  • Basic anchor links (`<a href="...">`), including how `href="#id"` jumps to an element with that id.
  • Basic HTML document structure from the earlier projects in this course.
  • It helps, but isn't required, to have seen basic CSS selectors before this project.

Project Structure

The page is one file, `gallery.html`, with a `<header>`, a `<main>` holding the gallery grid, and a set of overlay `<div id="...">` elements (one per photo) placed right after the grid — each overlay is only shown when the page's URL fragment matches its id, using the CSS `:target` pseudo-class. That's the entire "lightbox": no JavaScript, just anchors, ids, and one CSS rule.

Every photo is a `<figure>` containing an `<img>` and a `<figcaption>` — this pairing exists specifically so a caption is programmatically tied to its image, unlike a `<div><img><p>caption</p></div>`, where nothing in the markup itself says the paragraph describes the image above it.

Step 1: Scaffold the Page and Header

Start with the boilerplate and a simple header.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trail &amp; Peak Photography - Gallery</title>
<meta name="description" content="A gallery of landscape photography from mountain trails around the world.">
</head>
<body>
<header>
<h1>Trail &amp; Peak Photography</h1>
<p>A small gallery of landscape shots from recent hikes.</p>
</header>
<!-- The gallery grid and lightbox overlays are added inside <main> in the steps that follow -->
</body>
</html>

Step 2: Structure One Gallery Item

Each photo is a `<figure>` — the element specifically built for content that stands alone with its own caption, like an illustration, diagram, or photo. Inside it, `<figcaption>` is the caption, and the association is automatic: assistive tech announces the figcaption as this image's caption, something a plain nearby paragraph can never do.

<!-- <figure> pairs self-contained content with its own caption; <figcaption> is that
caption, and the two are programmatically linked - unlike a <div> + nearby <p> -->
<figure>
<img src="https://placehold.co/400x300" alt="Sunrise over a mountain ridge with fog in the valley below" width="400" height="300">
<!-- alt describes what's IN the photo, for when it fails to load or is read aloud -->
<figcaption>Sunrise, Dhauladhar Range</figcaption>
<!-- figcaption is the visible caption - distinct in purpose from the alt text above it -->
</figure>

Step 3: Build the Full Image Grid

Repeating that pattern six times, inside a `<main>` wrapped around a grid container, builds the full gallery. Every `alt` describes the photo's content, while every `figcaption` gives it a human title — the two attributes serve different audiences and shouldn't just repeat each other.

<main>
<h2>Gallery</h2>
<!-- Plain <div> is fine here: it's a pure layout container with no independent meaning of its own -->
<div class="gallery-grid">
<figure>
<img src="https://placehold.co/400x300" alt="Sunrise over a mountain ridge with fog in the valley below" width="400" height="300">
<figcaption>Sunrise, Dhauladhar Range</figcaption>
</figure>
<figure>
<img src="https://placehold.co/400x300" alt="Alpine lake reflecting snow-capped peaks" width="400" height="300">
<figcaption>Mirror Lake</figcaption>
</figure>
<figure>
<img src="https://placehold.co/400x300" alt="Narrow dirt trail cutting through a pine forest" width="400" height="300">
<figcaption>The Pine Trail</figcaption>
</figure>
<figure>
<img src="https://placehold.co/400x300" alt="Rocky summit above the cloud line at sunset" width="400" height="300">
<figcaption>Above the Clouds</figcaption>
</figure>
<figure>
<img src="https://placehold.co/400x300" alt="Waterfall cascading over moss-covered rocks" width="400" height="300">
<figcaption>Hidden Falls</figcaption>
</figure>
<figure>
<img src="https://placehold.co/400x300" alt="Wildflower meadow with mountains in the background" width="400" height="300">
<figcaption>Meadow in Bloom</figcaption>
</figure>
</div>
</main>

Step 4: Link Thumbnails to a Lightbox

Now the "lightbox" behavior: wrap each thumbnail's `<img>` in an anchor linking to a matching id, like `href="#photo-1"`. Clicking it changes the page's URL fragment, which is exactly the hook the next step's CSS uses to show a full-size overlay. No JavaScript is written for this — it's a genuine feature built entirely out of anchors and ids.

<figure>
<!-- Wrapping the thumbnail in a link to #photo-1 changes the page's URL fragment on
click - the :target CSS rule (added in Step 5) reacts to that fragment to reveal the overlay -->
<a href="#photo-1">
<img src="https://placehold.co/400x300" alt="Sunrise over a mountain ridge with fog in the valley below" width="400" height="300">
</a>
<figcaption>Sunrise, Dhauladhar Range</figcaption>
</figure>
<!-- Every other figure in the grid gets the same treatment: #photo-2, #photo-3, and so on -->

Step 5: Build the Lightbox Overlay Markup

Each lightbox overlay is a plain `<div id="photo-1">` placed after the grid, hidden by default and shown only when the URL fragment matches its id — that is the `:target` selector doing the work in CSS. A "Close" link inside sets `href="#"`, which clears the match and hides the overlay again.

<!-- One overlay per photo, placed after the gallery grid. Hidden by default in CSS;
shown only when its id matches the current URL fragment (the #photo-1 from Step 4) -->
<div id="photo-1" class="lightbox">
<div class="lightbox-content">
<img src="https://placehold.co/800x600" alt="Sunrise over a mountain ridge with fog in the valley below">
<p>Sunrise, Dhauladhar Range</p>
<a href="#" class="lightbox-close">&times; Close</a>
<!-- href="#" clears the URL fragment, which un-matches :target and hides this overlay again -->
</div>
</div>
<!-- Repeated as id="photo-2" through id="photo-6", one overlay per gallery photo -->

Step 6: Add the Footer

Finally, a simple footer closes out the page.

<footer>
<p>&copy; 2026 Trail &amp; Peak Photography.</p>
</footer>

Complete Code

Here is the full gallery page with all six photos, all six lightbox overlays, and the `:target`-driven CSS that makes the lightbox work.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trail &amp; Peak Photography - Gallery</title>
<meta name="description" content="A gallery of landscape photography from mountain trails around the world.">
<style>
* { box-sizing: border-box; }
body { font-family: Arial, Helvetica, sans-serif; margin: 0; color: #222; line-height: 1.6; background: #fafafa; }
header { background: #2b3a55; color: #fff; text-align: center; padding: 30px 20px; }
main { max-width: 900px; margin: 0 auto; padding: 20px; }
/* auto-fit + minmax reflows the grid from 3 columns down to 1 as the viewport narrows */
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }
figure { margin: 0; background: #fff; border: 1px solid #ddd; border-radius: 6px; overflow: hidden; }
figure img { display: block; width: 100%; height: auto; }
figure a { display: block; }
figcaption { padding: 10px; font-weight: bold; text-align: center; }
footer { text-align: center; padding: 20px; background: #2b3a55; color: #d7dce6; }
/* Lightbox: hidden by default, shown only when its id matches the URL fragment */
.lightbox {
display: none; /* Hidden until :target matches */
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.85); align-items: center; justify-content: center;
}
.lightbox:target { display: flex; } /* :target matches only the element whose id is the current URL fragment */
.lightbox-content { background: #fff; padding: 16px; border-radius: 6px; text-align: center; max-width: 90%; }
.lightbox-content img { max-width: 100%; max-height: 70vh; border-radius: 4px; }
.lightbox-close { display: inline-block; margin-top: 10px; color: #2b3a55; font-weight: bold; text-decoration: none; }
</style>
</head>
<body>
<header>
<h1>Trail &amp; Peak Photography</h1>
<p>A small gallery of landscape shots from recent hikes.</p>
</header>
<main>
<h2>Gallery</h2>
<div class="gallery-grid">
<figure>
<a href="#photo-1">
<img src="https://placehold.co/400x300" alt="Sunrise over a mountain ridge with fog in the valley below" width="400" height="300">
</a>
<figcaption>Sunrise, Dhauladhar Range</figcaption>
</figure>
<figure>
<a href="#photo-2">
<img src="https://placehold.co/400x300" alt="Alpine lake reflecting snow-capped peaks" width="400" height="300">
</a>
<figcaption>Mirror Lake</figcaption>
</figure>
<figure>
<a href="#photo-3">
<img src="https://placehold.co/400x300" alt="Narrow dirt trail cutting through a pine forest" width="400" height="300">
</a>
<figcaption>The Pine Trail</figcaption>
</figure>
<figure>
<a href="#photo-4">
<img src="https://placehold.co/400x300" alt="Rocky summit above the cloud line at sunset" width="400" height="300">
</a>
<figcaption>Above the Clouds</figcaption>
</figure>
<figure>
<a href="#photo-5">
<img src="https://placehold.co/400x300" alt="Waterfall cascading over moss-covered rocks" width="400" height="300">
</a>
<figcaption>Hidden Falls</figcaption>
</figure>
<figure>
<a href="#photo-6">
<img src="https://placehold.co/400x300" alt="Wildflower meadow with mountains in the background" width="400" height="300">
</a>
<figcaption>Meadow in Bloom</figcaption>
</figure>
</div>
</main>
<!-- One overlay per photo; :target in the stylesheet shows whichever one matches the URL fragment -->
<div id="photo-1" class="lightbox">
<div class="lightbox-content">
<img src="https://placehold.co/800x600" alt="Sunrise over a mountain ridge with fog in the valley below">
<p>Sunrise, Dhauladhar Range</p>
<a href="#" class="lightbox-close">&times; Close</a>
</div>
</div>
<div id="photo-2" class="lightbox">
<div class="lightbox-content">
<img src="https://placehold.co/800x600" alt="Alpine lake reflecting snow-capped peaks">
<p>Mirror Lake</p>
<a href="#" class="lightbox-close">&times; Close</a>
</div>
</div>
<div id="photo-3" class="lightbox">
<div class="lightbox-content">
<img src="https://placehold.co/800x600" alt="Narrow dirt trail cutting through a pine forest">
<p>The Pine Trail</p>
<a href="#" class="lightbox-close">&times; Close</a>
</div>
</div>
<div id="photo-4" class="lightbox">
<div class="lightbox-content">
<img src="https://placehold.co/800x600" alt="Rocky summit above the cloud line at sunset">
<p>Above the Clouds</p>
<a href="#" class="lightbox-close">&times; Close</a>
</div>
</div>
<div id="photo-5" class="lightbox">
<div class="lightbox-content">
<img src="https://placehold.co/800x600" alt="Waterfall cascading over moss-covered rocks">
<p>Hidden Falls</p>
<a href="#" class="lightbox-close">&times; Close</a>
</div>
</div>
<div id="photo-6" class="lightbox">
<div class="lightbox-content">
<img src="https://placehold.co/800x600" alt="Wildflower meadow with mountains in the background">
<p>Meadow in Bloom</p>
<a href="#" class="lightbox-close">&times; Close</a>
</div>
</div>
<footer>
<p>&copy; 2026 Trail &amp; Peak Photography.</p>
</footer>
</body>
</html>
Live Preview

Sample Run

Open `gallery.html` in a browser, or use the live preview above, and try the following:

  • Click any thumbnail — the URL gains a fragment like `#photo-3` and a full-size overlay of that photo appears, built entirely with anchors, ids, and one CSS `:target` rule.
  • Click "Close" inside the overlay — the fragment clears and the overlay disappears, back to the plain grid.
  • Use the browser's back button after opening a photo — because the lightbox state lives in the URL fragment, the browser's normal history navigation opens and closes it correctly.
  • Resize the window narrower — the gallery grid reflows from three columns down to two, then one, using CSS Grid's `auto-fit` behavior.

Extend This Project

  • Add "Next" and "Previous" links inside each lightbox overlay that jump straight to the neighboring photo's id.
  • Add a numbered caption ("Photo 3 of 6") inside each overlay using static text per overlay.
  • Swap the plain grid `<div>` for a `<ul>`/`<li>` structure and compare whether that is more or less semantically correct for a photo grid — there is a real debate here worth exploring.
  • Add `loading="lazy"` to every thumbnail `<img>` so off-screen photos don't load until the user scrolls near them.
  • Add a photographer credit line under each image using a second line of text inside the same `<figcaption>`.

Summary

You built a real image gallery with properly captioned `<figure>`/`<figcaption>` pairs and a genuinely working lightbox, driven entirely by anchor links, element ids, and the `:target` CSS selector — no JavaScript required. Understanding how far plain HTML and CSS can go before you need a scripting language is exactly the kind of judgment that separates markup that merely displays information from markup that behaves like a real interface.