webdevpuneet.com

Lazy Load Image Grid — Free HTML CSS JS Snippet

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

CategoryLayouts

Copy & paste

Source Code

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

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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

📚
IntersectionObserver lazy loading reference
Study a clear example of a single observer watching every image in a grid, with rootMargin of 200 pixels starting loads just before they appear.
🖼️
Photo and media galleries
Reduce initial page weight by loading each tile only as it nears the viewport, with a shimmer skeleton shown until the image arrives.
🛍️
Portfolio and product listings
Defer offscreen product images, using an internally scrollable grid so the effect is easy to demonstrate.
🎓
Performance teaching
Demonstrate performance-conscious image loading, with a fade-in transition so images appear gracefully instead of popping in.

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.