Image Gallery

CSS · Lesson 48 of 55 8 min read Updated

Photo galleries appear everywhere: hotel rooms, product photos, event pictures, a designer's portfolio. A good gallery fits any screen, keeps its thumbnails tidy even when the pictures have different shapes, and lets visitors open a bigger view with the mouse, a finger or the keyboard.

In this lesson you will build one step by step. It brings together CSS Grid, aspect-ratio and object-fit from Sizing, Aspect Ratio & Overflow, and the <dialog> element from Details, Summary & Dialog.

The HTML: a list of figures

A gallery is a set of similar items, so a list is a good container. Each item is a <figure>: the image plus its visible caption in <figcaption>.

HTML
<ul class="gallery">
  <li>
    <figure>
      <img src="https://course.ethicscomputer.in/assets/samples/portrait.svg" width="400" height="600"
           alt="A tall brick clock tower with a round white clock face, under a blue sky">
      <figcaption>The old clock tower, poster colours by Kabir</figcaption>
    </figure>
  </li>
  <!-- more items -->
</ul>

A few rules make this markup work well:

  • The alt text describes what the picture shows, for people who cannot see it. The caption adds context, such as the title and the artist. Screen readers read both, so do not repeat one in the other.
  • width and height are the image's real size. The browser uses them to reserve space before the image loads, so the page does not jump.
  • Add loading="lazy" when the gallery sits further down a long page, so images load only as the visitor scrolls near them. Leave it off images that are visible as soon as the page opens. Both points are covered in HTML Images.

A responsive grid with tidy thumbnails

The grid is the one-line pattern from CSS Grid Basics: as many columns as fit, each at least 10rem wide. min(100%, 10rem) stops a column from being wider than the screen on a very small phone.

Real pictures rarely share one shape. This gallery has a wide landscape, a tall tower, a square avatar, a long logo and a small icon. Left alone, they would make a ragged grid. Two properties fix that:

  • aspect-ratio: 4 / 3 gives every image box the same shape.
  • object-fit: cover fills that box and crops whatever does not fit, without stretching the picture.
HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Gallery grid</title>
  <style>
    body { margin: 0; padding: 1rem; font-family: system-ui, sans-serif; }
    .gallery {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));
      gap: 1rem;
      margin: 0;
      padding: 0;
      list-style: none;
    }
    .gallery figure { margin: 0; }
    .gallery img {
      box-sizing: border-box;
      display: block;
      width: 100%;
      height: auto;
      aspect-ratio: 4 / 3;
      object-fit: cover;
      border-radius: 8px;
      background: #f1f5f9;
    }
    .gallery .crop-top { object-position: top; }
    .gallery .show-whole { object-fit: contain; padding: 0.75rem; }
    .gallery figcaption { margin-top: 0.4rem; font-size: 0.875rem; color: #475569; }
  </style>
</head>
<body>
  <h1>Kochi College Art Club: 2026 show</h1>
  <ul class="gallery">
    <li><figure>
      <img src="https://course.ethicscomputer.in/assets/samples/landscape-400.svg" width="400" height="250"
           alt="Blue and green hills beside a winding river, with an orange sun rising">
      <figcaption>Sunrise in the hills, watercolour by Priya</figcaption>
    </figure></li>
    <li><figure>
      <img class="crop-top" src="https://course.ethicscomputer.in/assets/samples/portrait.svg" width="400" height="600"
           alt="A tall brick clock tower with a round white clock face, under a blue sky">
      <figcaption>The old clock tower, poster colours by Kabir</figcaption>
    </figure></li>
    <li><figure>
      <img src="https://course.ethicscomputer.in/assets/samples/avatar.svg" width="200" height="200"
           alt="Cartoon of a smiling person with dark hair and a green top">
      <figcaption>Self-portrait, digital art by Rohan</figcaption>
    </figure></li>
    <li><figure>
      <img class="show-whole" src="https://course.ethicscomputer.in/assets/samples/logo.svg" width="240" height="80"
           alt="Chai Corner logo: a brown tea cup with steam beside the shop name">
      <figcaption>Logo for a tea stall, by Meera</figcaption>
    </figure></li>
    <li><figure>
      <img class="show-whole" src="https://course.ethicscomputer.in/assets/samples/icon.svg" width="64" height="64"
           alt="A yellow star on a purple rounded square">
      <figcaption>App icon, by Farhan</figcaption>
    </figure></li>
  </ul>
</body>
</html>

Drag the editor narrower and wider: the number of columns changes, but every thumbnail keeps the same shape. Look closely at three choices:

  • The clock tower uses object-position: top. With plain cover, the middle of the picture is kept and the top of the tower is cut off; top keeps the roof and the clock instead.
  • The logo and icon use object-fit: contain with some padding. Cropping is fine for scenery, but a cropped logo loses its name. contain shows the whole image and lets the grey background fill the spare space.
  • display: block removes the small gap that appears under images, because images are inline by default and sit on the text baseline like letters.

Try removing aspect-ratio: 4 / 3 to see the ragged version, then put it back.

Hover and focus effects

Visitors should be able to open each picture. Wrap each image in a link to the full-size file, and give the link a gentle zoom on hover. Anything that reacts to the mouse must react to the keyboard too, so the same rule uses :focus-visible:

CSS
.thumb {
  display: block;
  overflow: hidden;          /* keeps the zoomed image inside its box */
  border-radius: 8px;
}
.thumb img {
  transition: scale 250ms ease-out;
}
.thumb:hover img,
.thumb:focus-visible img {
  scale: 1.06;
}
.thumb:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .thumb img { transition: none; }
}

The overflow: hidden on the link clips the slightly enlarged image, so the zoom happens inside the thumbnail. The outline is not clipped, because an element's own outline is drawn outside its box. Keep the captions always visible rather than showing them only on hover: touch screen users have no hover, so they would never see them.

A lightbox with dialog

A lightbox shows a large version of the picture over a dimmed page. The <dialog> element is made for this. Opened with showModal(), it puts a backdrop behind itself, moves focus inside, closes with Esc and returns focus to the thumbnail afterwards. A few lines of script connect the thumbnails to it.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Gallery with lightbox</title>
  <style>
    body { margin: 0; padding: 1rem; font-family: system-ui, sans-serif; }
    .gallery {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));
      gap: 1rem;
      margin: 0;
      padding: 0;
      list-style: none;
    }
    .gallery figure { margin: 0; }
    .gallery figcaption { margin-top: 0.4rem; font-size: 0.875rem; color: #475569; }
    .thumb { display: block; overflow: hidden; border-radius: 8px; }
    .thumb img {
      box-sizing: border-box;
      display: block;
      width: 100%;
      height: auto;
      aspect-ratio: 4 / 3;
      object-fit: cover;
      background: #f1f5f9;
      transition: scale 250ms ease-out;
    }
    .thumb:hover img, .thumb:focus-visible img { scale: 1.06; }
    .thumb:focus-visible { outline: 3px solid #f59e0b; outline-offset: 3px; }
    .crop-top { object-position: top; }
    .gallery .show-whole { object-fit: contain; padding: 0.75rem; }
    @media (prefers-reduced-motion: reduce) { .thumb img { transition: none; } }

    .lightbox {
      width: min(90vw, 44rem);
      padding: 1rem;
      border: 0;
      border-radius: 10px;
    }
    .lightbox::backdrop { background: rgb(0 0 0 / 0.8); }
    .lightbox figure { margin: 0; }
    .lightbox img {
      display: block;
      width: 100%;
      height: auto;
      max-height: 70dvh;
      object-fit: contain;
    }
    .lightbox figcaption { margin-top: 0.5rem; text-align: center; }
    .lightbox form { text-align: center; }
    .lightbox button { margin-top: 0.75rem; padding: 0.5rem 1.25rem; font: inherit; }
  </style>
</head>
<body>
  <h1>Kochi College Art Club: 2026 show</h1>
  <ul class="gallery">
    <li><figure>
      <a class="thumb" href="https://course.ethicscomputer.in/assets/samples/landscape.svg"><img src="https://course.ethicscomputer.in/assets/samples/landscape-400.svg" width="400" height="250"
         alt="Blue and green hills beside a winding river, with an orange sun rising"></a>
      <figcaption>Sunrise in the hills, watercolour by Priya</figcaption>
    </figure></li>
    <li><figure>
      <a class="thumb" href="https://course.ethicscomputer.in/assets/samples/portrait.svg"><img class="crop-top" src="https://course.ethicscomputer.in/assets/samples/portrait.svg" width="400" height="600"
         alt="A tall brick clock tower with a round white clock face, under a blue sky"></a>
      <figcaption>The old clock tower, poster colours by Kabir</figcaption>
    </figure></li>
    <li><figure>
      <a class="thumb" href="https://course.ethicscomputer.in/assets/samples/avatar.svg"><img src="https://course.ethicscomputer.in/assets/samples/avatar.svg" width="200" height="200"
         alt="Cartoon of a smiling person with dark hair and a green top"></a>
      <figcaption>Self-portrait, digital art by Rohan</figcaption>
    </figure></li>
    <li><figure>
      <a class="thumb" href="https://course.ethicscomputer.in/assets/samples/logo.svg"><img class="show-whole" src="https://course.ethicscomputer.in/assets/samples/logo.svg" width="240" height="80"
         alt="Chai Corner logo: a brown tea cup with steam beside the shop name"></a>
      <figcaption>Logo for a tea stall, by Meera</figcaption>
    </figure></li>
  </ul>

  <dialog class="lightbox" aria-labelledby="lightbox-caption">
    <figure>
      <img src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Blue and green hills beside a winding river, with an orange sun rising">
      <figcaption id="lightbox-caption">Sunrise in the hills, watercolour by Priya</figcaption>
    </figure>
    <form method="dialog">
      <button>Close</button>
    </form>
  </dialog>

  <script>
    const lightbox = document.querySelector(".lightbox");
    const bigImage = lightbox.querySelector("img");
    const bigCaption = lightbox.querySelector("figcaption");

    document.querySelector(".gallery").addEventListener("click", (event) => {
      const link = event.target.closest(".thumb");
      if (!link) return;
      event.preventDefault();   // stay on this page instead of opening the file
      bigImage.src = link.href;
      bigImage.alt = link.querySelector("img").alt;
      bigCaption.textContent = link.closest("figure").querySelector("figcaption").textContent;
      lightbox.showModal();
    });
  </script>
</body>
</html>

Click a thumbnail, or Tab to one and press Enter. The large picture opens over a dark backdrop. Close it with the button or Esc, and focus lands back on the thumbnail you opened, so a keyboard user does not lose their place.

Some details worth noticing:

  • The thumbnails are real links to the full-size files. If the script fails to load, a click still opens the picture. The script only takes over with preventDefault().
  • The first thumbnail uses the small landscape-400.svg file, while its link points to the large landscape.svg. Real galleries do the same: small files for the grid, big files only when asked for. Responsive Images shows how srcset takes this further.
  • In the lightbox, width: 100% with object-fit: contain and max-height: 70dvh fits any shape of picture inside the screen without stretching it. dvh is the dynamic viewport height, which stays correct on phones when the browser's address bar slides in and out.
  • aria-labelledby names the dialog after the caption, so a screen reader announces which picture opened.

Common mistakes

  • Images without width and height. The page jumps as each picture loads.
  • object-fit: cover on logos, charts or screenshots. Important parts get cropped. Use contain for them.
  • Captions only on hover. Touch screen users never see them. Keep captions visible.
  • Alt text like "image1.jpg" or "photo". Describe what the picture shows.
  • A lightbox built from a plain <div>. You then have to handle focus, Esc and the backdrop yourself. A modal <dialog> does it for you.

Key points

  • Mark up a gallery as a list of <figure> elements, each with an <img> and a <figcaption>.
  • repeat(auto-fit, minmax(min(100%, 10rem), 1fr)) gives a responsive grid without media queries.
  • aspect-ratio makes every thumbnail the same shape; object-fit: cover crops photos and contain keeps logos whole.
  • object-position chooses which part of a cropped picture stays visible.
  • Pair every :hover effect with :focus-visible, and keep captions visible.
  • A modal <dialog> makes an accessible lightbox; link thumbnails to the full files so they work without script.

Exercise

Make a gallery for Isha's sketchbook with four items: the landscape, the clock tower, the avatar and the star icon from the sample images. Use square thumbnails (aspect-ratio: 1) in a grid whose columns are at least 8rem wide. Crop the landscape, tower and avatar with cover, but show the icon whole with contain. Give each picture a caption and correct alt text, and add a coloured outline when a thumbnail link is hovered or focused.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Isha's sketchbook</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 1rem; }
    .gallery {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr));
      gap: 0.75rem;
      padding: 0;
      list-style: none;
    }
    figure { margin: 0; }
    .gallery a { display: block; border-radius: 6px; }
    .gallery a:hover,
    .gallery a:focus-visible { outline: 3px solid #7c3aed; outline-offset: 2px; }
    .gallery img {
      display: block;
      width: 100%;
      height: auto;
      aspect-ratio: 1;
      object-fit: cover;
      border-radius: 6px;
      background: #f3f4f6;
    }
    .gallery .whole { object-fit: contain; }
    figcaption { font-size: 0.875rem; margin-top: 0.3rem; }
  </style>
</head>
<body>
  <ul class="gallery">
    <li><figure>
      <a href="https://course.ethicscomputer.in/assets/samples/landscape.svg"><img src="https://course.ethicscomputer.in/assets/samples/landscape-400.svg" width="400" height="250" alt="Hills and a river at sunrise"></a>
      <figcaption>Morning view</figcaption>
    </figure></li>
    <li><figure>
      <a href="https://course.ethicscomputer.in/assets/samples/portrait.svg"><img src="https://course.ethicscomputer.in/assets/samples/portrait.svg" width="400" height="600" alt="A tall brick clock tower under a blue sky"></a>
      <figcaption>Clock tower</figcaption>
    </figure></li>
    <li><figure>
      <a href="https://course.ethicscomputer.in/assets/samples/avatar.svg"><img src="https://course.ethicscomputer.in/assets/samples/avatar.svg" width="200" height="200" alt="Cartoon of a smiling person"></a>
      <figcaption>A friend</figcaption>
    </figure></li>
    <li><figure>
      <a href="https://course.ethicscomputer.in/assets/samples/icon.svg"><img class="whole" src="https://course.ethicscomputer.in/assets/samples/icon.svg" width="64" height="64" alt="A yellow star on a purple square"></a>
      <figcaption>Star badge</figcaption>
    </figure></li>
  </ul>
</body>
</html>

aspect-ratio: 1 makes every thumbnail square, cover crops the three pictures neatly, and the whole class switches the icon to contain. The outline appears for both the mouse and the keyboard.

Quick Quiz

1. What does grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr)) do?

2. A logo is shown in a 4:3 thumbnail box. Which object-fit value keeps the whole logo visible without distorting it?

3. Why should gallery thumbnails be links to the full-size images?

4. What does a <dialog> opened with showModal() give a lightbox for free?

Learn CSS with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call