Source Code

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

Lazy Load Image Grid — Free HTML CSS JS Snippet

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

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.

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.