Image Grid View in HTML and CSS
Build a responsive image grid with semantic HTML, CSS Grid, responsive sources, accessible alt text, and reliable cropping choices.
Use CSS Grid for the layout and real <img> elements for content images. Define responsive columns, reserve space with image dimensions, and choose object-fit: cover when every tile should have the same shape or object-fit: contain when the entire source image must remain visible. Add srcset/sizes when the same image is available at multiple resolutions.
1. A complete responsive image grid
This example is a working page. Save it as index.html, place the referenced files in an images/ directory, and open it in a browser.
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Accessible image grid</title>
<style>
:root { color-scheme: light dark; }
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, sans-serif;
line-height: 1.5;
background: Canvas;
color: CanvasText;
}
main { width: min(100% - 2rem, 72rem); margin: 2rem auto; }
.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1rem;
list-style: none;
padding: 0;
margin: 0;
}
.gallery li { min-width: 0; }
.gallery img {
display: block;
width: 100%;
aspect-ratio: 4 / 3;
height: auto;
object-fit: cover;
object-position: center;
border-radius: .5rem;
background: #ddd;
}
@media (prefers-reduced-motion: no-preference) {
.gallery img { transition: transform .2s ease; }
.gallery a:hover img { transform: scale(1.02); }
}
</style>
</head>
<body>
<main>
<h1>Field notes</h1>
<ul class='gallery'>
<li>
<img src='images/mountain-640.jpg'
srcset='images/mountain-320.jpg 320w,
images/mountain-640.jpg 640w,
images/mountain-1280.jpg 1280w'
sizes='(min-width: 60rem) 25vw, (min-width: 40rem) 33vw, 100vw'
width='1280' height='960'
alt='Snow-covered mountain above a pine valley'
loading='eager' decoding='async'>
</li>
<li>
<img src='images/coast-640.jpg'
srcset='images/coast-320.jpg 320w,
images/coast-640.jpg 640w,
images/coast-1280.jpg 1280w'
sizes='(min-width: 60rem) 25vw, (min-width: 40rem) 33vw, 100vw'
width='1280' height='960'
alt='Rocky coast beside calm blue water'
loading='lazy' decoding='async'>
</li>
<li>
<img src='images/forest-640.jpg'
srcset='images/forest-320.jpg 320w,
images/forest-640.jpg 640w,
images/forest-1280.jpg 1280w'
sizes='(min-width: 60rem) 25vw, (min-width: 40rem) 33vw, 100vw'
width='1280' height='960'
alt='Sunlight between tall trees in a forest'
loading='lazy' decoding='async'>
</li>
</ul>
</main>
</body>
</html>
The list gives the gallery a useful structure for screen readers. The grid uses fluid tracks, so columns become narrower and then stack as space disappears. The aspect-ratio creates a predictable tile shape; object-fit controls how each source fills that shape.
2. Choose between cover and contain
| Goal | CSS | Result |
|---|---|---|
| Uniform cards with no bands | object-fit: cover |
The image keeps its aspect ratio and fills the box; edges can be cropped. |
| Show every pixel of the source | object-fit: contain |
The complete image remains visible; empty bands may appear. |
| Allow the source height to define the tile | Remove the fixed aspect-ratio and use height: auto |
Tiles can have different heights, so rows will not be uniform. |
MDN documents these object-fit behaviors in its image and media guidance (MDN image styling). If a face, product, chart, or other subject must stay visible, use contain or adjust the crop with object-position, such as object-position: 50% 20%.
3. Make the grid responsive
Fluid columns
repeat(auto-fit, minmax(min(100%, 16rem), 1fr)) lets the browser fit as many tracks as the available width allows. The inner min(100%, 16rem) prevents a single card from overflowing a very narrow viewport.
Explicit breakpoints
Use media queries when the design requires known column states:
.gallery {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 40rem) {
.gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 64rem) {
.gallery { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
There is no universal best breakpoint or column count. Check the actual card width, text size, zoom level, and content length. W3C WAI describes media queries with Grid as one technique for reflowing columns (WAI C32).
Prevent overflow
Use minmax(0, 1fr) for explicit tracks, min-width: 0 on grid children, and max-width: 100% on images. These rules allow long URLs, captions, and replaced elements to shrink with the track.
4. Use responsive image files
srcset lists available resource widths and sizes tells the browser how wide the image will render. The browser can then select a suitable file instead of downloading one large source for every device. MDN explains this selection process in its responsive images guide.
<img src='photo-640.jpg'
srcset='photo-320.jpg 320w, photo-640.jpg 640w, photo-1280.jpg 1280w'
sizes='(min-width: 60rem) 25vw, (min-width: 40rem) 33vw, 100vw'
width='1280' height='960'
alt='Description of the photograph'>
Use <picture> when the image itself should change by condition, such as an art-directed crop or a modern format with a fallback:
<picture>
<source srcset='photo.avif' type='image/avif'>
<source srcset='photo.webp' type='image/webp'>
<img src='photo.jpg' width='1280' height='960' alt='Description of the photograph'>
</picture>
5. Accessibility and loading
- Write concise, useful
alttext for meaningful images. If an image is purely decorative and conveys no information, use an empty alternative (alt='') so assistive technology can skip it. See MDN’s image guidance. - Provide intrinsic
widthandheightvalues. They reserve the correct aspect ratio before the file arrives and reduce layout movement; the MDN<img>reference covers these attributes. - Use
loading='lazy'for images initially below the viewport. Keep the first visible image eager unless another loading strategy is deliberate. - Keep keyboard focus visible when images are links, and give linked images an accessible name through their alternative text or an accompanying label.
- Set
height: autowhen preserving the source ratio. If both dimensions are forced, pair them withobject-fitto avoid distortion.
For fitting images during reflow, W3C WAI documents max-width and height as an advisory technique (WAI C37).
6. Captions, links, and a lightbox
Use <figure> and <figcaption> when a caption belongs to one image:
<figure class='card'>
<a href='images/mountain-2048.jpg'>
<img src='images/mountain-640.jpg' width='640' height='480'
alt='Snow-covered mountain above a pine valley' loading='lazy'>
</a>
<figcaption>North ridge after snowfall</figcaption>
</figure>
A lightbox is optional. If you add one, make the dialog keyboard accessible, trap focus while it is open, provide a labelled close button, and preserve a normal link to the full image so the gallery still works without JavaScript.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Images look stretched | Both dimensions are forced without a fitting rule. | Use height: auto, or set object-fit: cover/contain in a fixed box. |
| Important content is cut off | cover crops the source. |
Use contain, change object-position, or create an art-directed crop with <picture>. |
| Blank bands appear | contain preserves the whole image inside a different-shaped box. |
Accept the bands, match the box ratio, or use cover when cropping is acceptable. |
| Layout jumps while loading | No intrinsic dimensions or aspect ratio were supplied. | Add accurate width/height attributes or an aspect-ratio. |
| Grid overflows on mobile | Tracks or children have an unshrinkable minimum width. | Use minmax(0, 1fr), min-width: 0, and max-width: 100%. |
| Wrong responsive file downloads | sizes does not describe the rendered slot. |
Update each media condition to match the grid’s actual column width. |
| Lazy images do not appear | The URL is wrong, the response is not an image, or CSS hides the element. | Check the Network panel, response status and content type, then inspect computed dimensions. |
| Alt text is repeated or meaningless | The filename or caption was copied into alt. |
Describe the image’s purpose; use alt='' for decorative artwork. |
8. Performance, reliability, and cost
- Generate a small, medium, and large source for each image and connect them with
srcset. Avoid downloading a file far larger than its rendered slot. - Reserve layout space with dimensions, then lazy-load below-the-fold items. This reduces unnecessary work while keeping the initial layout stable.
- Use an image CDN or a build-time optimizer when your project has many originals. Set cache headers appropriate to your deployment and use immutable filenames when files are content-addressed.
- Test at narrow widths, browser zoom, high contrast settings, slow connections, and with images disabled. The gallery should remain understandable even if a request fails.
- Do not promise a universal column count or performance percentage. Results depend on source dimensions, format, network, device, and the number of images.
9. Or skip the browser setup
If you need rendered screenshots of a gallery for documentation, visual regression, or a social preview, ScreenshotNeo captures a URL with one GET request. It accepts cookie and consent banners like a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets before capture, and each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; the response identifies the result with X-Page-Verdict and X-Billed headers.
See the ScreenshotNeo API documentation for the complete option set.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The API also supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, clicks, selector/delay/network-idle waits, request blocking, headers, cookies, user agents, Authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, usage reporting, PDFs, HTML/CSS to image, and an MCP server with take_screenshot, get_page_info, and capture_pdf. Every feature is on every plan. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
10. FAQ
Should I use CSS Grid or Flexbox?
Use Grid when rows and columns form a two-dimensional gallery. Flexbox is useful for one-dimensional toolbars or captions around the grid.
Can I keep every image’s natural height?
Yes. Remove the fixed aspect ratio and use height: auto, but expect uneven row heights unless you choose a masonry-style layout.
Does loading='lazy' guarantee faster pages?
No. It requests off-screen images later, but results depend on browser heuristics, distance from the viewport, image size, and network conditions.
When should I use picture instead of srcset?
Use srcset/sizes for resolution choices of the same image. Use picture when the source should change by format or art direction.
How do I verify the grid at different sizes?
Resize the viewport and test browser zoom, keyboard navigation, slow networks, missing images, and a screen reader. Confirm that no track or caption creates horizontal scrolling.


