Lazy Load Image Grid — Free HTML CSS JS Snippet

Lazy Load Image Grid · Layouts · Plain HTML, CSS & JS · Live preview

CategoryLayouts

What's included

Features

Single IntersectionObserver instance watches all grid images rather than one observer per tile
rootMargin of 200px starts loading images slightly before they enter the visible area
Internally scrollable grid (max-height plus overflow-y: auto) makes the effect demonstrable without page scroll
Animated shimmer skeleton placeholder shown before each image loads
Fade-in transition applied only after the native image load event fires, not immediately on src assignment
observer.unobserve() called per image after it loads so each image is only ever processed once
data-src attribute keeps the browser from requesting any image until it is actually near the viewport

About this UI Snippet

Lazy Load Image Grid — IntersectionObserver-Driven Image Loading

Screenshot of the Lazy Load Image Grid snippet rendered live

This snippet is a grid of image tiles that defer loading their actual images until each tile scrolls near the visible area, using the browser's IntersectionObserver API instead of loading everything upfront.

Placeholders instead of real sources

Each <img> starts with no src attribute at all — only a data-src holding the real picsum.photos URL. Without a real src, the browser never requests the image, and each tile shows an animated shimmer skeleton background (a CSS gradient animation) in the meantime, making the "not yet loaded" state visually obvious.

One observer watching every tile

A single IntersectionObserver instance is created once and told to observe() every placeholder image. Its rootMargin: '200px' option expands the effective viewport by 200px on all sides, so images start loading slightly before they actually scroll into view rather than popping in right at the edge. The root option is set to the grid container itself (not the page), since the grid scrolls internally via max-height and overflow-y: auto.

Load-once semantics

When an entry's isIntersecting becomes true, the callback copies data-src into the real src attribute, which triggers the actual network request. Once the image's native load event fires, a .ll-loaded class fades it in via a CSS opacity transition and the shimmer skeleton is removed. Critically, observer.unobserve(entry.target) is called immediately after triggering the load — so each image is only ever loaded once, and the observer stops doing any work for tiles that have already loaded.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Hand this snippet's HTML, CSS, and JavaScript to an AI coding assistant like Claude and ask it to adapt "Lazy Load Image Grid" to your project — restyle it to match your design system, wire it up to real data instead of the static example, or port it to the framework you're building in.

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 layouts component called "Lazy Load Image Grid" using plain HTML, CSS, and vanilla JavaScript — no framework, no build step.

Requirements:
- Match the structure and behavior of the "Lazy Load Image Grid" snippet from the UI Snippets Library.
- Keep the markup semantic and the styling self-contained (no external dependencies beyond what the original snippet uses).
- Keep the JavaScript vanilla, with no framework runtime required.
- Make it easy to restyle via CSS custom properties or class overrides so it can be dropped into a real project.

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.

Source Code

<div class="ll-wrap">
  <h2 class="ll-heading">Scroll to lazy-load images</h2>
  <div class="ll-grid" id="llGrid"></div>
</div>

Step by step

How to Use

  1. 1
    Load the snippetClick "Lazy Load Image Grid" in the sidebar to load its HTML, CSS, and JS into the editor panels. The preview updates instantly.
  2. 2
    Edit the codeModify any panel — HTML, CSS, or JS. The preview refreshes as you type. Use Reset in each panel header to restore the original.
  3. 3
    Preview on devicesClick the Mobile (375px), Tablet (768px), or Desktop buttons in the preview header to check responsiveness.
  4. 4
    Export in your formatClick "HTML" to download a standalone file, "JSX" for a React component, "Tailwind" for a React + Tailwind CSS component, "Tailwind HTML" for a standalone HTML file with Tailwind CDN, "Vue" for a Vue 3 SFC with <template>/<script setup>/<style scoped>, or "Angular" for a standalone Angular .component.ts file. "Copy all" copies the full code to clipboard.
  5. 5
    Save your versionClick "Save as", type a name, and press Enter. Your snippet saves to IndexedDB and appears in the Saved tab.

Real-world uses

Common Use Cases

Reference for IntersectionObserver-based lazy loading
A clean, from-scratch implementation of the pattern browsers now also offer natively via loading="lazy".
Photo galleries and media grids
Reduce initial page weight on any grid with more images than fit on one screen.
Portfolio and product listing pages
Defer offscreen product or portfolio images until the user actually scrolls to them.
Teaching performance-conscious image loading
Demonstrates rootMargin tuning and proper observer cleanup with unobserve.

Got questions?

Frequently Asked Questions

The native attribute works well for simple cases, but IntersectionObserver gives full control over the threshold, root element (useful here since the grid scrolls internally, not the page), rootMargin pre-loading distance, and lets you trigger custom effects like the fade-in and skeleton removal precisely when loading starts and completes.

It expands the observer's effective intersection boundary outward by 200px in every direction, so an image is considered "intersecting" — and starts loading — while it is still 200px away from actually entering the visible grid area, giving the network request a head start before the user scrolls to it.

Once an image has loaded, there is no reason to keep paying the (small) cost of the observer tracking its intersection state. Calling unobserve stops the observer from firing further callbacks for that element, which is the standard "load once" pattern for lazy-loaded content.

The grid itself is internally scrollable via max-height and overflow-y: auto, so the relevant scrolling container is the grid, not the page. Setting root to the grid element makes the observer calculate intersection relative to that internal scroll area.