Source Code

<div class="container py-5">
  <div class="btn-group mb-4" role="group" id="bsmgFilter">
    <button type="button" class="btn btn-outline-dark active" data-cat="all">All</button>
    <button type="button" class="btn btn-outline-dark" data-cat="nature">Nature</button>
    <button type="button" class="btn btn-outline-dark" data-cat="city">City</button>
    <button type="button" class="btn btn-outline-dark" data-cat="people">People</button>
  </div>

  <div class="bsmg-masonry" id="bsmgGrid">
    <img src="https://picsum.photos/id/1015/500/650" data-cat="nature" class="bsmg-img" alt="Mountain river">
    <img src="https://picsum.photos/id/1011/500/400" data-cat="people" class="bsmg-img" alt="Portrait">
    <img src="https://picsum.photos/id/1016/500/750" data-cat="city" class="bsmg-img" alt="City skyline">
    <img src="https://picsum.photos/id/1025/500/500" data-cat="people" class="bsmg-img" alt="Dog portrait">
    <img src="https://picsum.photos/id/1043/500/620" data-cat="nature" class="bsmg-img" alt="Field">
    <img src="https://picsum.photos/id/1031/500/450" data-cat="city" class="bsmg-img" alt="Street">
    <img src="https://picsum.photos/id/1035/500/700" data-cat="nature" class="bsmg-img" alt="Forest">
    <img src="https://picsum.photos/id/1027/500/500" data-cat="people" class="bsmg-img" alt="Crowd">
    <img src="https://picsum.photos/id/1044/500/580" data-cat="city" class="bsmg-img" alt="Bridge">
  </div>
</div>

<div class="modal fade" id="bsmgModal" tabindex="-1">
  <div class="modal-dialog modal-dialog-centered modal-lg">
    <div class="modal-content bg-dark">
      <div class="modal-body p-0 text-center position-relative">
        <img id="bsmgModalImg" src="" class="img-fluid w-100" alt="Enlarged view">
        <button class="btn btn-light bsmg-nav bsmg-prev" id="bsmgPrev">&laquo;</button>
        <button class="btn btn-light bsmg-nav bsmg-next" id="bsmgNext">&raquo;</button>
        <button type="button" class="btn-close btn-close-white position-absolute top-0 end-0 m-2" data-bs-dismiss="modal"></button>
      </div>
    </div>
  </div>
</div>

Bootstrap Masonry Image Gallery — Free HTML CSS JS Snippet

Bootstrap Masonry Image Gallery · Layouts · Plain HTML, CSS & JS · Live preview

What's included

Features

True CSS column-count masonry layout, no JavaScript layout calculation required
break-inside: avoid prevents images from being sliced across column boundaries
Category filter re-flows the masonry layout automatically since hidden items simply leave the column flow
Real Bootstrap modal component reused for every image via a single bootstrap.Modal instance
Prev/Next navigation always cycles within the currently filtered image set, recomputed on each open
Circular index wrapping so Prev/Next never gets stuck at the first or last image
Keyboard Left/Right arrow support while the modal is open
Responsive column-count drops from three to two to one via media queries

About this UI Snippet

Bootstrap Masonry Image Gallery — HTML, CSS & JavaScript

Screenshot of the Bootstrap Masonry Image Gallery snippet rendered live

A masonry layout needs images of different heights to flow into balanced columns without leftover gaps, and the simplest way to get that without a JavaScript layout library is CSS multi-column layout: .bsmg-masonry sets column-count: 3 and column-gap: 0.75rem, and each .bsmg-img gets break-inside: avoid so the browser's own column-balancing algorithm never slices an image in half across a column break. Because column layout — unlike CSS Grid — flows items down each column before wrapping to the next, images of varying aspect ratios naturally interlock instead of leaving the ragged same-height rows a plain row/col grid would produce.

Category filtering works the same way as a typical Bootstrap btn-group filter: each image carries its category in a data-cat attribute, and clicking a filter button calls applyFilter(cat), which toggles a bsmg-hidden class (display: none) on every image that doesn't match, while also moving Bootstrap's active class onto only the clicked button. Because column layout reflows automatically as elements are hidden, filtering the gallery re-balances the remaining images into fresh columns with no manual re-layout code needed.

The lightbox is a real Bootstrap modal component, instantiated once with new bootstrap.Modal(modalEl) and reused for every image rather than creating a new modal instance per click. The detail that makes the Prev/Next cycling correct is in openModal(img): it calls visibleImages() — which filters allImages down to whatever currently lacks bsmg-hidden — and stores that as currentSet at the moment the modal opens, then finds the clicked image's position in it with indexOf. This is the specific edge case the requirements call out: if the currently applied filter is "Nature", Prev/Next inside the modal must only step through the nature photos, not the full nine-image gallery, and recomputing currentSet fresh on every open (rather than caching it once globally) means switching filters and reopening the modal always cycles the correct, currently-filtered subset.

showAt(index) wraps the index with (index + len) % len, so clicking Prev on the first image or Next on the last one loops around to the opposite end instead of getting stuck, and the same navigation is also wired to the Left/Right arrow keys while the modal is open.

The modal's dark bg-dark content area and btn-close-white close button are deliberate choices rather than defaults: a lightbox showing a photograph reads much better against a near-black background than Bootstrap's default white modal, since a white surround competes visually with the photo's own edges and colors, and the inverted close-button variant keeps that control legible against the dark background instead of disappearing into it.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to add a fade/zoom transition when the modal image changes between Prev/Next clicks, or to lazy-load the gallery images with the loading="lazy" attribute and a blur-up placeholder for slower connections.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a Bootstrap 5.3 masonry image gallery with a lightbox using the real Bootstrap CDN (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble Bootstrap.

Requirements:
- A CSS column-count based masonry grid of at least nine images with varying heights, flowing into columns without gaps, using break-inside: avoid so images are never sliced across a column break.
- A category filter using a Bootstrap btn-group above the gallery that shows/hides images by a data-category attribute, letting the masonry columns re-balance automatically as items are hidden or shown.
- Clicking any visible image opens it enlarged in a real Bootstrap modal component (instantiated via the bootstrap.Modal JS API), with Prev/Next buttons inside the modal.
- The Prev/Next buttons must cycle only through the currently filtered set of images (recomputed at the moment the modal opens), with the index wrapping around at both ends.
- Support Left/Right arrow key navigation while the modal is open, in addition to the Prev/Next buttons.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Load the snippetNine images of varying heights appear arranged into three flowing masonry columns with no gaps.
  2. 2
    Click "Nature"The grid re-balances to show only the nature photos, flowing into columns without leftover empty space.
  3. 3
    Click any visible imageA real Bootstrap modal opens showing that image enlarged, with Prev/Next arrow buttons.
  4. 4
    Click Next repeatedlyThe modal cycles only through the nature photos currently shown, wrapping back to the first after the last.
  5. 5
    Close the modal and click "All"The full nine-image gallery reappears, re-flowed into masonry columns.
  6. 6
    Reopen an image and use arrow keysLeft and Right arrow keys navigate the modal the same way the Prev/Next buttons do.

Real-world uses

Common Use Cases

Photography and portfolio galleries
The primary use case for masonry — showcasing images of mixed aspect ratios without cropping them to a uniform grid.
CART
Product image galleries in e-commerce
Combine with Image Carousel with Thumbnails to offer both a browsing grid and a focused single-product view.
Learning CSS multi-column layout
A practical, dependency-free reference for column-count masonry as an alternative to JavaScript grid libraries.
Event or travel photo recaps
Filter recap photos by category (venue, people, highlights) the same way this demo filters by nature, city, and people, similar to filtering testimonials in Testimonial Grid with Ratings.
Blog and content media libraries
Pair with Lightbox Gallery Modal patterns for a full-featured, filterable media browser inside a CMS.

Got questions?

Frequently Asked Questions

Yes. Keep the image list and active category in component state, derive the visible array with a filter before rendering, and store the modal open state and current index in the same state object; the CSS column-count masonry and modal markup need no changes, only the class-toggling logic moves into conditional class bindings.

CSS multi-column layout (column-count) automatically distributes block-level children down each column in source order, and because images have naturally varying heights, this produces the balanced, gap-free masonry look without any manual position calculation.

openModal() recomputes visibleImages() — the current non-hidden image set — at the exact moment the modal opens and stores it as currentSet, so all subsequent Prev/Next clicks index into that filtered array rather than the full unfiltered gallery.

The break-inside: avoid CSS rule on each .bsmg-img tells the browser's column-balancing algorithm to move a whole image to the next column rather than splitting it across a column boundary.

Yes — replace the btn-group and modal with Tailwind's own button group styling and a hand-built dialog (or Bootstrap's bundled JS just for the modal component), while keeping the column-count masonry CSS and the filtering/navigation JavaScript exactly as is, since neither depends on Bootstrap-specific behavior beyond the Modal API.

Yes — add another img.bsmg-img element with a new data-cat value and a matching filter button with the same data-cat value; both the masonry layout and the filtering logic read that attribute directly, requiring no other code changes.