Skeleton Product Cards Loading

How to Create a Skeleton Loading for Product Cards with Vanilla JavaScript

html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="./app.js" defer></script>
    <link rel="stylesheet" href="./style.css">
    <title>Document</title>
</head>

<body>

    <div id="product-card-container">

        <div class="product-card-item">
            <div class="img-container skeleton">
                <img width="100"
                    src="https://assets.adidas.com/images/w_383,h_383,f_auto,q_auto,fl_lossy,c_fill,g_auto/bc1e0a82cc724171b06b712bfb03cb1f_9366/gazelle-indoor-shoes.jpg">
            </div>
            <ul>
                <li class="skeleton">Gazelle Indoor Shoes</li>
                <li class="skeleton">$120.00</li>
            </ul>

            <button>Add to cart</button>
        </div>

        <div class="product-card-item">
            <div class="img-container skeleton">
                <img width="100"
                    src="https://assets.adidas.com/images/h_840,f_auto,q_auto,fl_lossy,c_fill,g_auto/a046f90f47c042d69294a97601139ffc_9366/Handball_Spezial_Shoes_Blue_BD7632_02_standard.jpg">
            </div>
            <ul>
                <li class="skeleton">Handball Spezial Shoes</li>
                <li class="skeleton">$110.00</li>
            </ul>

            <button>Add to cart</button>
        </div>

        <div class="product-card-item">
            <div class="img-container skeleton">
                <img width="100"
                    src="https://assets.adidas.com/images/h_2000,f_auto,q_auto,fl_lossy,c_fill,g_auto/8f0511c4303f43adaff26bdf3ec78806_9366/Handball_Spezial_Shoes_Green_JH5444_01_00_standard.jpg">
            </div>
            <ul>
                <li class="skeleton">Gazelle Indoor Shoes</li>
                <li class="skeleton">$120.00</li>
            </ul>

            <button>Add to cart</button>
        </div>

        <div class="product-card-item">
            <div class="img-container skeleton">
                <img width="100"
                    src="https://assets.adidas.com/images/h_840,f_auto,q_auto,fl_lossy,c_fill,g_auto/51fa97b24eb5404b809c29a39a87fca4_9366/Sambae_Shoes_White_JI1349_01_standard.jpg">
            </div>
            <ul>
                <li class="skeleton">Sambae Shoes Price</li>
                <li class="skeleton">$110.00</li>
            </ul>

            <button>Add to cart</button>
        </div>

        <div class="product-card-item">
            <div class="img-container skeleton">
                <img width="100"
                    src="https://assets.adidas.com/images/h_2000,f_auto,q_auto,fl_lossy,c_fill,g_auto/8bd96d0fe05a48b0800b8170a096c8f9_9366/Adizero_Prime_X_2.0_STRUNG_Shoes_Green_IH5683_HM1.jpg">
            </div>
            <ul>
                <li class="skeleton">Adizero Prime X 2.0 STRUNG Shoes</li>
                <li class="skeleton">$300.00</li>
            </ul>

            <button>Add to cart</button>
        </div>
    </div>

</body>

</html>

css

* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    list-style: none;
}

#product-card-container {
    width: 100%;
    display: flex;
    flex-direction: row;
    gap: 10px;
    flex-wrap: wrap;
    justify-content: space-evenly;
    padding: 10px;

    & .product-card-item {
        width: 250px;
        display: flex;
        flex-direction: column;
        gap: 10px;

        & .img-container {
            height: 254px;
            border-radius: 10px;
            overflow: hidden;

            & img {
                width: 100%;
            }
        }

        & ul {
            display: flex;
            flex-direction: column;
            gap: 10px;

            & li:nth-child(1) {
                font-size: 14px;
            }

            & .skeleton {
                overflow: hidden;
                border-radius: 5px;
            }
        }

        & button {
            padding: 10px;
            cursor: pointer;
        }
    }
}

.skeleton {
    position: relative;
    transition: opacity 0.90s, visibility 0.90s;

    &::after {
        content: '';
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        z-index: 10;
        background: linear-gradient(90deg, #eee, #f9f9f9, #eee);
        background-size: 200%;
        animation: skeleton-animate 1s infinite reverse;
    }
}

@keyframes skeleton-animate {
    0% {
        background-position: -100% 0;
    }

    100% {
        background-position: 100% 0;
    }
}

javascript

window.addEventListener('load', () => {
    const images = document.querySelectorAll('.img-container img');

    images.forEach(img => {
        if (img.complete) {
            removeSkeleton(img);
        } else {
            img.addEventListener('load', () => removeSkeleton(img));
            img.addEventListener('error', () => removeSkeleton(img));
        }
    });

    const skeletonTexts = document.querySelectorAll('li.skeleton');
    skeletonTexts.forEach(el => el.classList.remove('skeleton'));
});

function removeSkeleton(img) {
    const container = img.closest('.img-container');
    if (container && container.classList.contains('skeleton')) {
        container.classList.remove('skeleton');
    }
}