โ† All guides

Skeleton loading for WooCommerce product grids

On a slow connection a shop page arrives in two stages: the text first, then the product images one by one. In between, the grid is a set of empty boxes โ€” or worse, boxes with no height that jump open as each image lands. A skeleton is the fix most apps use: a soft grey shape with a moving shine where the image will be, so the page looks like it is loading rather than broken, and nothing moves when the picture arrives.

First, make sure nothing jumps

A skeleton fills the space an image will take, so that space has to be known before the image arrives. WooCommerce prints width and height on its product images, and browsers use them to reserve the right box โ€” as long as your theme's CSS keeps height: auto and nothing strips the attributes. To check, slow the network down in your browser's developer tools and reload the shop. If the grid opens up as images arrive, fix that first. When your thumbnails are cropped square (Appearance โ†’ Customize โ†’ WooCommerce โ†’ Product Images), this is enough:

.woocommerce ul.products li.product img {
	aspect-ratio: 1 / 1;
	object-fit: cover;
}

The skeleton: CSS from the first paint

An image is a box with a background like any other element, and its background shows until the picture paints over it. So the shimmer can sit on the image itself: no extra markup, and no waiting for JavaScript โ€” because the CSS is in the page head, the skeleton is there from the very first paint.

.woocommerce ul.products li.product img:not(.is-loaded) {
	background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%) 0 0 / 200% 100%;
	animation: shop-skeleton 1.4s linear 8;
}

@keyframes shop-skeleton {
	to {
		background-position: -200% 0;
	}
}

@media (prefers-reduced-motion: reduce) {
	.woocommerce ul.products li.product img:not(.is-loaded) {
		animation: none;
	}
}

The 8 matters: the shine runs eight times, about eleven seconds, and stops. An image that never arrives โ€” a deleted file, a dead link โ€” would otherwise shimmer for as long as the page is open. Visitors who set their device to reduce motion get the grey shape without the shine.

The JavaScript: take it away when the image arrives

The CSS alone would leave the grey behind images with transparent areas, and keep the animation running behind every picture. A few lines mark each image once it has arrived:

const shopImages = '.woocommerce ul.products li.product img';

const markLoaded = (img) => {
	// A lazy-load placeholder is a data: image; wait for the real file.
	if (!img.currentSrc.startsWith('data:')) {
		img.classList.add('is-loaded');
	}
};

// Images that finish from now on, including those a filter or "load more" adds.
// load and error do not bubble, so listen in the capture phase.
for (const type of ['load', 'error']) {
	document.addEventListener(type, (event) => {
		if (event.target instanceof HTMLImageElement && event.target.matches(shopImages)) {
			markLoaded(event.target);
		}
	}, true);
}

// Images that finished before this code ran.
document.querySelectorAll(shopImages).forEach((img) => {
	if (img.complete) {
		markLoaded(img);
	}
});

One listener on the document catches every image, the ones in the page and the ones added later, so there is nothing to set up again after a product filter reloads the grid. The data: check is for themes and plugins that lazy-load by putting a tiny placeholder in src and swapping in the real file later: the placeholder loads instantly, and without the check the skeleton would disappear before the product image is there.

In StyleScript, the free plugin for your own CSS and JavaScript, the CSS goes in the CSS box โ€” which prints in the page head, so the skeleton shows from the first paint โ€” and the script in the JavaScript box, loaded in the footer.

If your shop's grid is built with WooCommerce's blocks or a page builder's product widget, the markup differs: inspect one product image in your browser and use its class in place of the selector above.

What to avoid

The no-code version

StyleScript Pro, the add-on to the free plugin, has Skeleton Loading as a ready-made effect under Appearance โ†’ StyleScript โ†’ Effects. You pick which elements it covers โ€” with presets for WooCommerce product grids, Flatsome product boxes and banners, Elementor images and galleries, the image and gallery blocks, or every lazy-loaded image โ€” and set the colours, the shine's speed, darker colours for visitors in dark mode, how long it shows at least and when it stops at the latest. Its CSS is printed in the head, so it is there from the first paint, and its script โ€” under a kilobyte โ€” handles the lazy loaders that swap a placeholder for the real image, Flatsome's included. A second mode covers boxes that are empty until something fills them, such as the list a product filter reloads.

Keep your custom code in one place

StyleScript holds your CSS, JavaScript and header/footer code apart from the theme, prints each where it belongs, and keeps every version you save.

See what StyleScript does ย Pricing