CSS for responsive image galleries at all viewport widths
CSS is pure mayhem, but whatever, it’s what we’ve got. For a long time I’ve had reasonably good responsive image galleries. Really the last remaining roadblocks has been with those galleries where I’ve decided I need to vary the width of images for aesthetic reasons…
Well, today I improved the style behaviour in half of those cases.
The only caveat to this set of styles is that you have to be careful when using the wide-first
or wide-last
classes as they can leave an orphan image out in space if you don’t consider how many (odd or even) images are in the gallery or if they are of very different aspect ratios.
But besides those I’ve found this to be pretty rock solid, if a little verbose.
Here’s the relevant cascade,
root {
--body-width: 820px;
--gallery-spacing: clamp(2px, 4px, 1rem);
--gallery-transition: 0.3s ease-in-out;
}
.gallery {
display: grid;
gap: var(--gallery-spacing);
width: 100%;
margin: 2rem 0;
}
.gallery > img,
.gallery > picture {
width: 100%;
height: 100%;
}
.gallery > img,
.gallery > picture img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: var(--image-radius);
transition: transform var(--gallery-transition);
}
.gallery > img:hover,
.gallery > picture:hover img {
transform: scale(1.01);
cursor: zoom-in;
}
/* Dynamic columns based on number of children */
.gallery:has(> :nth-child(1):nth-last-child(1)) {
grid-template-columns: 1fr;
}
.gallery:has(> :nth-child(1):nth-last-child(2)),
.gallery:has(> :nth-child(2):nth-last-child(1)) {
grid-template-columns: repeat(2, 1fr);
}
.gallery:has(> :nth-child(1):nth-last-child(3)),
.gallery:has(> :nth-child(2):nth-last-child(2)),
.gallery:has(> :nth-child(3):nth-last-child(1)) {
grid-template-columns: repeat(3, 1fr);
}
.gallery:has(> :nth-child(n + 4)) {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
/* Layout variants */
.gallery.one-wide {
grid-template-columns: 1fr !important;
}
.gallery.two-wide {
grid-template-columns: repeat(2, 1fr) !important;
}
.gallery.four-wide {
grid-template-columns: repeat(auto-fit, minmax(185px, 1fr));
}
.gallery.wide-first > :first-child,
.gallery.wide-last > :last-child {
grid-column: 1 / -1;
max-height: 80vh;
}
@media (max-width: 768px) {
.gallery:has(> :nth-child(1):nth-last-child(3)),
.gallery:has(> :nth-child(2):nth-last-child(2)),
.gallery:has(> :nth-child(3):nth-last-child(1)) {
grid-template-columns: repeat(2, 1fr);
}
.gallery.four-wide,
.gallery:has(> :nth-child(n + 4)) {
grid-template-columns: repeat(2, 1fr);
}
.gallery:has(> :nth-child(3):nth-last-child(1)) > :last-child {
grid-column: span 2;
}
}
@media (max-width: 480px) {
.gallery {
grid-template-columns: 1fr;
}
.gallery.four-wide,
.gallery:has(> :nth-child(n + 4)) {
grid-template-columns: repeat(2, 1fr);
}
/* Disable hover effects on touch devices */
.gallery > img:hover,
.gallery > picture:hover img {
transform: none;
}
}
/* Full viewport width variant */
.gallery.full-viewport {
width: 100vw;
margin-left: calc(50% - 50vw);
margin-right: calc(50% - 50vw);
}
.gallery.full-viewport img,
.gallery.full-viewport picture {
transform: none !important;
border-radius: 0;
}
/* Square aspect ratio class */
.gallery.square-items > img,
.gallery.square-items > picture img {
aspect-ratio: 1;
}