Loaders✨ Premium

Image Grid Skeleton

2 × 2 grid of 260 × 260 px: four 125 × 125 px cells, 12 px radius, 1.8 s ease-in-out shimmer on ::after. A 0 / 0.15 / 0.30 / 0.45 s cascade carried by the --skel-delay custom property, set on each cell and inherited by the pseudo-element — the four tiles light up one after another.

CSSGridImages

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Loaders has 29 effects, including 4 free. Explore the category →

3 usage examples

Variant picker — four product images loading — Image Grid Skeleton example 1

① Variant picker — four product images loading

WhenSide panel or modal for product configuration: the buyer selects a colour from four options, each represented by a square image.
WhyA 2 × 2 grid of uniform squares matches exactly four equal-sized visuals. The 12 px radius suits a shopping interface. Four colours have no order among them: here, remove the style="--skel-delay: …" attributes so all four cells shimmer together — a synchronous shimmer is less distracting than a cascade when the buyer is not ranking the options.
SettingsWidth set to 200px → ~95 × 95 px cells, radius 8px, duration 1.4s, --skel-delay attributes removed (synchronous shimmer).
Recent posts — Instagram widget in a page footer — Image Grid Skeleton example 2

② Recent posts — Instagram widget in a page footer

WhenFooter or sidebar of a fashion or restaurant site: a 'Follow us' block shows the four latest photos before they load from the network.
WhyInstagram photos are always square; four 2 × 2 cells at 260 px reproduce the expected layout exactly. The subtle shimmer avoids drawing the eye away from navigation during load.
SettingsRadius 0 or 4px to match the native feed look, slightly warmer fill rgba(255,200,150,.06).
Cover thumbnail — four frames pulled from the video arrive one after another — Image Grid Skeleton example 3

③ Cover thumbnail — four frames pulled from the video arrive one after another

WhenThe 'Thumbnail' step of publishing a video: the editor grabs four frames from the cut (0:04, 0:31, 1:12, 2:05) and offers them as cover options. They are extracted in video order and appear as each one is ready.
WhyFour same-size options arriving in a known order: that is exactly what the 0 / 0.15 / 0.30 / 0.45 s cascade says — the first cell lights up before the second, which precedes the third… the offset replays the extraction order. A spinner or a bar would say 'loading' without saying how many or in what order; a list of lines would not say 'these are images'. Four 125 px squares in a 2 × 2 reserve the exact slot of each thumbnail: nothing jumps when they land.
SettingsDefault width 260px (125 × 125 px cells), 12 px radius, default cascade (--skel-delay 0 / 0.15 / 0.30 / 0.45 s), shimmer raised to rgba(255,255,255,.18) so the offset between cells is readable.

How it works

Structure. .grid-skel is a two-column CSS grid (grid-template-columns: 1fr 1fr, gap: 10px, width: 260px): each cell is (260 − 10) ÷ 2 = 125 px wide. Height is set by aspect-ratio: 1/1 → 125 × 125 px. The full grid is 260 × 260 px. Each cell is a div.skel-bone.grid-skel-img: fill rgba(255,255,255,.08), border-radius: 12px (overriding the 6 px from the base class), position: relative and overflow: hidden to contain the pseudo-element. No JS: the effect is entirely CSS.

Shimmer. The animation runs on ::after (absolute, inset: 0, content: ""). Its background is a linear-gradient(90deg, transparent 0, rgba(255,255,255,.1) 50%, transparent 100%) with background-size: 200% 100% — the gradient is twice the cell's width. The shimmerSlide keyframe moves background-position from -200% 0 to 200% 0: the bright stripe enters from the left, sweeps across the cell and exits right. Duration: 1.8 s ease-in-out, infinite. The 10% opacity peak stays subtle against the 8% grey fill.

Cascade — and why it goes through a custom property. The shipped HTML sets style="--skel-delay: .15s", .3s and .45s on three of the four cells (the first has no attribute), and .skel-bone::after declares animation-delay: var(--skel-delay, 0s). This detour is required: the animation lives on the pseudo-element, and an animation-delay set on an element does not propagate to its ::after — animation properties are not inherited, each pseudo-element has its own. A CSS custom property, on the other hand, is inherited by pseudo-elements: var(--skel-delay, 0s) reads the value written on the parent cell and falls back to 0s when none is set. Measured with the getAnimations() API: the four pseudo-elements report delay = 0 / 150 / 300 / 450 ms and four distinct background-position values at the same instant — the cascade is clearly visible. Counter-check: a style="animation-delay: .15s" written directly on the cell leaves all four ::after at delay=0ms and at the same position — perfect sync, no cascade.

Accessibility

  • prefers-reduced-motion not handled. Confirmed: no @media (prefers-reduced-motion: reduce) rule in the sold CSS. All four shimmers run continuously regardless of the system preference. Add @media (prefers-reduced-motion: reduce) { .skel-bone::after { animation: none } } — the grey cells remain visible and still convey a loading state, without motion.
  • A skeleton screen is a functional loading indicator, not pure decoration. The .grid-skel container should carry role="status" and aria-label="Loading images…" so a screen reader announces the waiting state. Remove these attributes and replace with aria-hidden="true" once the real content is inserted.
  • The cells are empty divs: no text, no icon. Set aria-hidden="true" on each .skel-bone to keep the accessibility tree clean, with only the container's announcement.
  • Integration. The grid has a fixed width (260 px): on narrow viewports, add max-width: 100% to .grid-skelgrid-template-columns: 1fr 1fr and aspect-ratio: 1/1 will adapt automatically.

Browser compatibility

Pure CSS, no JS. Requires CSS Grid (2019), aspect-ratio (2021), inset (2021), custom properties var() (2017) and @keyframes/animation (2012).

Chrome 88+✓ Full
Firefox 89+✓ Full
Safari 15+✓ Full
Edge 88+✓ Full
Mobile iOS 15+✓ Full
Android Chrome 88+✓ Full

Without aspect-ratio (Safari 14 and older): cells collapse to zero height and become invisible. Fix: add height: 125px to .grid-skel-img. Without inset: replace with top:0;right:0;bottom:0;left:0 on .skel-bone::after.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="grid-skel">
  <div class="skel-bone grid-skel-img"></div>
  <div class="skel-bone grid-skel-img" style="--skel-delay: .15s"></div>
  <div class="skel-bone grid-skel-img" style="--skel-delay: .3s"></div>
  <div class="skel-bone grid-skel-img" style="--skel-delay: .45s"></div>
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Shimmer duration (CSS — .skel-bone::after { animation: 1.8s ease-in-out infinite … shimmerSlide }) 1.8 s Speed of the bright stripe sweep. 1 s = snappy shimmer; 2.5 s = slow, discreet pulse.
Grid width (CSS — .grid-skel { width: 260px }) 260 px Drives cell size: (width − gap) ÷ 2. 200px → ~95 × 95 px; 340px → ~165 × 165 px. Add max-width: 100% for fluid layouts.
Gap (CSS — .grid-skel { gap: 10px }) 10 px Gutter between the four cells. 6 px to tighten; 16 px for a card-like spacing.
Border radius (CSS — .grid-skel-img { border-radius: 12px }) 12 px 12 px for softly rounded corners. 0 for sharp squares (full-bleed photos); 50% for circles (4-up avatar grid).
Cell fill (CSS — .skel-bone { background: rgba(255,255,255,.08) }) rgba(255,255,255,.08) Resting fill visible between shimmer passes. On a light background, switch to rgba(0,0,0,.08).
Shimmer intensity (CSS — rgba(255,255,255,.1) in the .skel-bone::after gradient) rgba(255,255,255,.1) .06 = barely visible; .18 = clearly defined stripe. Keep a ≥ 2:1 ratio with the cell fill value.
Cascade (HTML — style="--skel-delay: .15s" on each cell, read by .skel-bone::after { animation-delay: var(--skel-delay, 0s) }) 0 / 0.15 / 0.30 / 0.45 s (the first cell has no attribute: 0s fallback) Offset between cells lighting up. Remove the attributes → synchronous shimmer; .25s, .5s, .75s → a more pronounced cascade. Always go through the custom property: an animation-delay written directly on the cell does not propagate to ::after, whereas a custom property is inherited by it.

FAQ

Because the shimmerSlide animation is declared on ::after, and a pseudo-element has its own animation properties: an animation-delay set on the parent cell is not passed down — animation-* properties are not inherited. CSS custom properties are: .skel-bone::after { animation-delay: var(--skel-delay, 0s) } therefore reads the value written in style="--skel-delay: .15s" on the cell, and falls back to 0s for the first one, which has no attribute. Counter-check with getAnimations(): with style="animation-delay: .15s" on the cell instead, all four pseudo-elements stay at delay=0ms and shimmer in perfect sync. If you generate the grid in a loop, write style="--skel-delay: …" on each cell with an increasing value.
fx-0469 Skeleton Card simulates a portrait card: one image rectangle at the top, followed by two text lines below — a single item with an image-plus-description structure. fx-0441 Image Grid Skeleton simulates a media gallery: four uniform squares arranged in a 2 × 2 grid, with no text. One signals the loading of mixed content (photo + description); the other, the loading of a pure image grid. They are not interchangeable.
Yes: the effect uses no JS. For a 3 × 3, add 5 .skel-bone.grid-skel-img elements (9 total) and set grid-template-columns to 1fr 1fr 1fr. For a 4 × 2, add 4 elements and use 1fr 1fr 1fr 1fr. Adjust width accordingly — e.g. 370px for four ~85 px columns with a 10 px gap. aspect-ratio: 1/1 handles the height automatically. Extend the cascade by setting --skel-delay to increasing values on the new cells (.6s, .75s…), or remove the attributes for a synchronous shimmer.