GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

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). Stacking two photos and dragging a handle between them is its own set, 9 Before After Slider UIs, and widening the panes themselves belongs to 9 Resizable Split Pane Layouts.

# 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

What moves

Four photos sit in an even 2×2 grid, and the css image hover lifts only the tile under the cursor, deepening its shadow to mark which one you're looking at.

Even 2-column grid
Open in a new tab

01 · Even 2-column grid — Measured area 3.564% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

grid-template-columns:repeat(2,1fr)gaptranslateY hover

In the code

It's the plainest layout there is, good for a 4-6 photo portfolio row or photos scattered through a blog post.

.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

What moves

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.

3-column grid, cropped to match
Open in a new tab

02 · 3-column grid, cropped to match — Measured area 3.778% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

aspect-ratio:4/3object-fit:coveroverflow:hidden

In the code

Built for a gallery of phone photos with mismatched ratios.

.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

What moves

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.

Auto-responsive grid, no media query
Open in a new tab

03 · Auto-responsive grid, no media query — Measured area 23.517% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

repeat(auto-fit,minmax())그리드 자동 배치opacity+scale 진입

In the code

Good for product galleries where photo count varies by category.

.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

What moves

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.

Multi-column waterfall (columns)
Open in a new tab

04 · Multi-column waterfall (columns) — Measured area 3.547% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

columns:2break-inside:avoidcolumn-gap

In the code

This is the classic Pinterest-style moodboard layout.

.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)

What moves

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.

Grid-row-span waterfall (keeps reading order)
Open in a new tab

05 · Grid-row-span waterfall (keeps reading order) — Measured area 2.089% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

grid-auto-rows:4pxgrid-row-end:span N읽는 순서 유지

In the code

Use it when publish order matters, like a news feed.

.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)

What moves

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.

Square grid lock (1:1)
Open in a new tab

06 · Square grid lock (1:1) — Measured area 3.329% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

aspect-ratio:1/1object-fit:coverscale hover

In the code

.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

What moves

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.

Featured-first layout
Open in a new tab

07 · Featured-first layout — Measured area 7.026% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

:nth-of-typegrid-column: 1 / 3grid-row: 1 / 3

In the code

Good for a blog header gallery that spotlights one hero photo.

.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

What moves

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.

Hover caption overlay
Open in a new tab

08 · Hover caption overlay — Measured area 4.308% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

translateY hover반투명 그러데이션opacity

In the code

Made for a travel gallery that labels each photo with a place name.

.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

What moves

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.

Click-to-open lightbox, zero JS
Open in a new tab

09 · Click-to-open lightbox, zero JS — Measured area 11.947% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

popovertarget::backdrop@starting-style

In the code

Pair <button popovertarget="lb-01"> with <div id="lb-01" popover> and the browser handles every bit of the open/close logic.

.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

css image gallery examples — where does it break?

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. Tiles that also tilt toward the cursor inside their own cell are covered in 3D Hover Effect CSS. 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

What we checked

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 the 04 or 05 css masonry grid 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. Galleries where a person drags the order around by hand are collected in Portfolio Gallery HTML CSS.

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.