GODRICH

9 CSS Image Gallery Grid Effects

CSS image gallery grid effects arrange photos into a grid or waterfall that reacts to hover and click, as pretty as the gallery you saved from Pinterest — but

Auto-plays · click a tile to jump to its section · all nine in one zip

These nine are ordered the way you actually hit problems while building a real gallery, not by popularity. Start with the plainest even grid (01), solve the real-world mess of mismatched photo ratios (02), add a responsive layout that resizes itself (03), compare two ways to stack photos of different heights into a waterfall (04-05), lock every tile into a perfect square or blow up one hero photo (06-07), then finish with the interactions people actually click: captions and a full-size lightbox (08-09).

# Name What moves JS
01 Even 2-column grid Tile translateY/scale None
02 3-col grid, cropped to match Photo scale, frame translateY None
03 Auto-responsive grid Tile opacity/scale entrance None
04 Multi-column waterfall Tile translateY None
05 Grid-row-span waterfall Tile scale None
06 Square grid lock (1:1) Photo scale, frame translateY None
07 Featured-first layout Tile scale None
08 Hover caption overlay Caption translateY/opacity None
09 Click-to-open lightbox Panel scale/opacity None

01Even 2-column grid

Four photos sit in an even 2×2 grid, and only the tile under the cursor lifts slightly with a deeper shadow to mark which one you're looking at. It's the plainest layout there is, good for a 4-6 photo portfolio row or photos scattered through a blog post.

grid-template-columns:repeat(2,1fr)gaptranslateY hover
.gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: $sp-2;
}
.tile {
  transition: transform $duration $easing, box-shadow $duration $easing;
}
.tile:hover, .tile:focus-visible {
  transform: translateY(-4px) scale(1.03);
  box-shadow: $shadow-raised;
}

023-col grid, cropped to match

Even when tall, wide, and square photos are mixed together, giving every image aspect-ratio: 4/3 and object-fit: cover locks every cell to the same size, and hovering zooms in only inside that frame. Built for a gallery of phone photos with mismatched ratios.

aspect-ratio:4/3object-fit:coveroverflow:hidden
.cell {
  aspect-ratio: 4 / 3;
  overflow: hidden;
}
.photo {
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1);
  transition: transform $duration $easing;
}
.cell:hover .photo, .cell:focus-within .photo {
  transform: scale(1.12);
}

03Auto-responsive grid

A single grid-template-columns: repeat(auto-fit, minmax(56px, 1fr)) line lets the column count adjust itself to the container width with zero media queries, and each new tile fades and scales into place. Good for product galleries where photo count varies by category.

repeat(auto-fit,minmax())그리드 자동 배치opacity+scale 진입
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(56px, 1fr));
  gap: $sp-2;
}
.tile {
  opacity: .35;
  transform: scale(.88);
  transition: transform $duration $easing, opacity $duration $easing;
}
.tile.is-visible {
  opacity: 1;
  transform: scale(1);
}

04Multi-column waterfall

Giving five photos just columns: 2 stacks photos of different heights into a gapless waterfall down each column with zero JavaScript, and break-inside: avoid stops a photo from being sliced in half across a column break. This is the classic Pinterest-style moodboard layout.

columns:2break-inside:avoidcolumn-gap
.gallery {
  columns: 2;
  column-gap: $sp-2;
}
.tile {
  break-inside: avoid;
  margin: 0 0 $sp-2;
  transition: transform $duration $easing;
}
.tile:hover, .tile:focus-visible {
  transform: translateY(-3px);
}

05Grid-row-span waterfall (keeps reading order)

Slicing grid-auto-rows into a tiny unit like 4px and pre-computing each photo's own grid-row-end: span N produces a waterfall that, unlike the column version, lays photos out left-to-right in reading order instead of top-to-bottom-then-next-column. Use it when publish order matters, like a news feed.

grid-auto-rows:4pxgrid-row-end:span N읽는 순서 유지
.gallery {
  display: grid;
  grid-auto-flow: row dense;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 4px;
  column-gap: $sp-2;
}
.tile--1 { grid-row-end: span 10; }
.tile--2 { grid-row-end: span 13; }
.tile--3 { grid-row-end: span 9; }

06Square grid lock (1:1)

Setting aspect-ratio: 1 / 1 and object-fit: cover on every tile lines up perfect squares like an Instagram feed regardless of the source photo's ratio, and hovering lifts the tile slightly while the photo inside zooms in.

aspect-ratio:1/1object-fit:coverscale hover
.cell {
  aspect-ratio: 1 / 1;
  overflow: hidden;
}
.photo {
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1);
  transition: transform $duration $easing;
}
.cell:hover .photo {
  transform: scale(1.1);
}

07Featured-first layout

Giving only the first photo grid-column: span 2 and grid-row: span 2 makes it four times larger than the rest, filling in smaller photos around it for a magazine-style layout from a single CSS selector. Good for a blog header gallery that spotlights one hero photo.

:nth-of-typegrid-column: 1 / 3grid-row: 1 / 3
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 56px);
  grid-template-rows: repeat(2, 56px);
  gap: $sp-2;
}
.tile:first-child {
  grid-column: 1 / 3;
  grid-row: 1 / 3;
}

08Hover caption overlay

A translucent band that's normally tucked just below the photo's edge slides fully into view via translateY on hover, bringing the caption text (title, location) up with it. Made for a travel gallery that labels each photo with a place name.

translateY hover반투명 그러데이션opacity
.caption {
  position: absolute; left: 0; right: 0; bottom: 0;
  transform: translateY(64%);
  opacity: 0;
  transition: transform $duration $easing, opacity $duration $easing;
}
.tile:hover .caption, .tile:focus-within .caption {
  transform: translateY(0);
  opacity: 1;
}

09Click-to-open lightbox, zero JS

Clicking a tile opens a popover holding the full-size photo using nothing but a popovertarget attribute, ::backdrop dims the page behind it, and @starting-style makes the photo start small and transparent before growing into view. Pair <button popovertarget="lb-01"> with <div id="lb-01" popover> and the browser handles every bit of the open/close logic.

popovertarget::backdrop@starting-style
.lightbox {
  opacity: 0;
  transform: scale(.8);
  transition: opacity $duration $easing, transform $duration $easing,
    overlay $duration $easing allow-discrete, display $duration $easing allow-discrete;
  @starting-style { opacity: 0; transform: scale(.8); }
}
.lightbox:popover-open { opacity: 1; transform: scale(1); }
.lightbox::backdrop { background: rgba($stage-ink, .7); opacity: 0; }
.lightbox:popover-open::backdrop { opacity: 1; }

Where it breaks — one real trap

With plain grid-auto-flow: row (no dense), the browser's placement cursor only moves forward, so a gap that opens after a tall span can stay unfilled for the rest of the layout instead of getting backfilled. Adding dense closes gaps like that by re-scanning from the top for every tile, but that same re-scan is what breaks strict DOM order: dense prioritizes filling gaps over DOM order, so depending on the span combination, a tile can render in a position that doesn't match its DOM order. This demo's own span values (10/13/9/12/10) happen to leave no gap either way, so here dense is cheap insurance rather than a fix for a visible hole — change the span numbers and that stops being true. Layout 04 (multi-column) never hits this because its order is strictly top-to-bottom-then-next-column by definition; layout 05 (grid row-span) carries the reordering risk the moment dense is switched on. If the order itself carries meaning — a publish date, say — either drop dense and accept the occasional gap, or open the vanilla folder guarded by the zip password 24p2nyh2 and hand-tune the span numbers so no gap forms in the first place.

Accessibility

With prefers-reduced-motion: reduce on, all nine turn off their transitions and loops and land straight on the finished state instead of a half-animated one. 01, 02, and 06 apply their zoom or lift instantly, 03's tiles appear already in place (full opacity, full size) with no fade-in, 04 and 05 skip the lift and sit still, 07's featured tile keeps its emphasis size without the pop, 08 leaves the caption permanently expanded, and 09's lightbox opens and closes without the scale-in transition. Item 09's trigger is a real <button>, so it opens from the keyboard (Tab, then Enter) exactly as it does from a click, and its popovertargetaction="hide" close button works the same way. The other eight items also wire their hover-only motion to :focus-visible or :focus-within so a keyboard user sees the same result a mouse user does. Browser support for the CSS Grid masonry proposal is tracked on MDN, and the Popover API's support table lives on MDN's Popover API page. If card-style hover effects are more your speed, the card hover collection and the glassmorphism collection cover that ground.

FAQ

What happens to the 09 lightbox in a browser that doesn't support the Popover API?

The popovertarget attribute is simply ignored by browsers that don't recognize it, so the button stays a plain button and clicking it does nothing — no layout breakage, no console error. Current Chrome, Edge, Safari, and Firefox already support it.

Should I use 04 or 05 for my waterfall?

If reading photos top-to-bottom-then-next-column is fine (a moodboard, a general gallery), the columns approach (04) is shorter and easier to maintain. If the layout needs to read left-to-right in strict order — a "newest first" feed, say — use the grid version (05), and budget for the dense reordering risk covered above.

How does the React version of 03's auto-responsive grid know the column count?

It doesn't compute one. AutoFitGallery never touches a resize listener or state — grid-template-columns: repeat(auto-fit, minmax()) reads only the container's width inside the browser's own layout engine, so it reflows on its own whenever the window changes size.

Enter the archive password

The password is inside this article. You will find it as you read.