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
- A skeleton over the whole page until everything has loaded. It hides text and images that are ready, and Google measures when the main content becomes visible (Largest Contentful Paint). Skeletons belong on the parts that are slow, not on top of the page.
- Skeletons on everything. An image that arrives in 50 milliseconds only flashes. Use them where images really are late: product grids, galleries, banners below a slow header.
- A flash for images that are almost there. If most images land within a fraction of a second, keep the skeleton up for a minimum of around 300 milliseconds, so it either shows properly or not at all.
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.