Skeleton to Content
Sept bones shimmer (1,8 s) dans une carte 280 px — avatar 48 px, 2 lignes d'en-tête, 3 lignes de texte, bloc image 100 px ; un clic ajoute .loaded : bones effacés en 500 ms (opacité + scale(.98)), contenu entrant décalé de 150 ms.
Vous aurez accès à la démo interactive avec un compte gratuit.
Cet effet fait partie d'Effect.Labs — 811 effets vanilla, une partie gratuite, une partie premium. Chargement comprend 29 effets, dont 4 gratuits. Découvrir la catégorie →
3 exemples d'usage



Comment ça marche
La carte .toggle-skel-card superpose deux couches dans le même bloc : .toggle-skel-bones (le squelette) et .toggle-skel-content (le contenu réel). Le squelette regroupe 7 bones — un cercle de 48 px représentant l'avatar, deux lignes d'en-tête (60 % et 40 % de largeur), trois lignes de texte (100 %, 90 %, 65 %) et un rectangle image de 100 px de hauteur. Chaque bone est en background: rgba(255,255,255,.08) ; un pseudo-élément ::after anime shimmerSlide — un linear-gradient à 10 % d'opacité qui balaie de −200 % à 200 % en 1,8 s ease-in-out en boucle infinie. Le contenu réel comprend un avatar en dégradé indigo-violet (#6366f1 → #a855f7) avec la lettre « A », le nom « Alice Durand », le handle @alice_dev, un paragraphe et un bloc image bleu-nuit.
La bascule repose sur la classe .loaded ajoutée ou retirée de #toggleSkelCard. Côté squelette : transition: opacity .5s, transform .5s — quand .loaded est présente, opacity: 0 et transform: scale(.98) effacent les bones en 500 ms, qui passent simultanément en position: absolute; inset: 20px pour céder l'espace au contenu. Côté contenu : display: none en base, display: block dès que .loaded est posée, puis transition: opacity .5s .15s, transform .5s .15s — le contenu entre de 0 à 1 en opacité et de translateY(8px) à 0, les deux avec 150 ms de décalage. Le bouton change de libellé : « Charger le contenu » → « Afficher le squelette ».
Pourquoi le clic est branché en JS et non par un attribut onclick. Le script livré est une IIFE — (function(){ … })() — et tout ce qui y est déclaré reste invisible depuis le scope global. Or un attribut onclick="maFonction()" est évalué dans ce scope global : une fonction enfermée dans l'IIFE n'y existe pas, et le clic lèverait un ReferenceError sans rien déclencher. Le code livré règle le problème à la source : il récupère #toggleSkelCard et #toggleSkelBtn par getElementById, sort immédiatement si l'un des deux manque (if (!card || !btn) return;), puis pose btn.addEventListener('click', …) depuis l'intérieur de l'IIFE — l'écouteur voit card et btn par fermeture, sans jamais passer par window. À chaque clic, il exécute card.classList.toggle('loaded') et remplace le libellé du bouton selon l'état. Le HTML ne porte donc aucun onclick et aucune fonction globale n'est créée : rien ne peut entrer en collision avec un autre effet copié sur la même page. Conséquence pour l'intégrateur : il n'existe pas de window.toggleSkeleton() — pour déclencher la bascule depuis votre propre code, agissez sur la classe .loaded de #toggleSkelCard ou cliquez le bouton par programme (btn.click()).
Accessibilité
- prefers-reduced-motion absent du CSS et du JS : le shimmer (1,8 s en boucle) et les transitions de bascule (500 ms) s'exécutent quelles que soient les préférences système. Mesuré sous émulation
reducedMotion: reduce:animationPlayState: runningsur les bones. Ajoutez@media (prefers-reduced-motion: reduce) { .skel-bone::after, .skel-bone-circle::after { animation: none } .toggle-skel-bones, .toggle-skel-content { transition: none } }. - Aucun attribut ARIA sur le composant : ni
role="status"niaria-livesur la carte, niaria-busypendant l'état squelette. Un lecteur d'écran ne sait pas que la carte est en cours de chargement. L'intégrateur doit ajouteraria-busy="true"sur.toggle-skel-carden mode squelette, basculer à"false"lors du changement d'état, et placer une zonearia-live="polite"hors de la carte pour annoncer la disponibilité du contenu. - Le bouton est un vrai
<button>, focalisable et activable au clavier, avec un libellé textuel visible que l'écouteur livré met à jour à chaque bascule (« Charger le contenu » / « Afficher le squelette »). Pour rendre l'état explicite aux technologies d'assistance, ajoutezaria-pressedsynchronisé avec.loadeddans ce même écouteur — en fixant alors le libellé : un bouton « Afficher le squelette » annoncé « activé » serait contradictoire. - Contenu réel sur fond sombre uniquement : les textes du contenu réel utilisent
rgba(255,255,255, …)— ils sont illisibles sur fond clair. Dans un vrai usage, adaptez les couleurs du contenu si votre interface supporte un thème clair.
Compatibilité navigateur
Utilise transition, @keyframes, linear-gradient, border-radius, rgba(), getElementById(), addEventListener() et classList.toggle(). Aucune dépendance externe, aucune variable globale.
Sans JavaScript, .loaded n'est jamais ajoutée : le squelette reste visible en boucle indéfiniment. Si #toggleSkelCard ou #toggleSkelBtn manque dans le DOM, le script sort avant de poser l'écouteur, sans erreur console — même résultat, squelette permanent. Sans @keyframes (IE 9 non préfixé), les bones s'affichent en aplat sans scintillement. Les transitions CSS sont ignorées sous IE 9 : la bascule serait instantanée.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="toggle-skel-wrapper">
<div class="toggle-skel-card" id="toggleSkelCard">
<div class="toggle-skel-bones">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<div class="skel-bone-circle" style="width:48px;height:48px;flex-shrink:0"></div>
<div style="flex:1">
<div class="skel-bone" style="height:14px;width:60%;margin-bottom:8px"></div>
<div class="skel-bone" style="height:10px;width:40%"></div>
</div>
</div>
<!-- 3 lignes de texte -->
<div class="skel-bone" style="height:10px;width:100%;margin-bottom:8px"></div>
<div class="skel-bone" style="height:10px;width:90%;margin-bottom:8px"></div>
<div class="skel-bone" style="height:10px;width:65%;margin-bottom:16px"></div>
<!-- bloc image -->
<div class="skel-bone" style="height:100px;width:100%;border-radius:10px"></div>
</div>
<div class="toggle-skel-content">
<!-- contenu réel — remplacer par vos données -->
</div>
</div>
<button class="toggle-skel-btn" id="toggleSkelBtn">Charger le contenu</button>
</div>
HTML + CSS + JS complet, prêt à coller — avec des centaines d'effets premium.
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
Durée du shimmer (CSS — animation: 1.8s ease-in-out 0s infinite sur ::after) |
1,8 s | Vitesse du balayage lumineux sur chaque bone. 1s pour un rendu plus vif ; 2,5s pour un effet plus doux. Tous les bones partagent la même durée et restent synchrones. |
Durée d'effacement du squelette (CSS — transition: opacity .5s, transform .5s sur .toggle-skel-bones) |
.5 s | Temps de disparition des bones lors de la bascule. .3s pour une transition plus rapide ; .8s si vous voulez un fondu long pendant une réponse API lente. |
Décalage d'entrée du contenu (CSS — délai dans transition: opacity .5s .15s, transform .5s .15s) |
.15 s de délai | Le contenu attend ce délai avant de commencer à entrer. 0s = entrée simultanée avec la disparition du squelette ; .3s = les bones ont presque disparu avant que le contenu apparaisse. |
Amplitude du glissement (CSS — transform: translateY(8px) sur .toggle-skel-content) |
8 px | Distance de l'entrée du contenu, du bas vers le haut. 4px pour un geste quasi imperceptible ; 20px pour une entrée plus affirmée. Changez en translateX(8px) pour une entrée latérale. |
Largeur du composant (CSS — .toggle-skel-wrapper { width: 280px }) |
280 px | Toutes les bones exprimées en pourcentage s'y rapportent. Passer à 100% adapte le composant à son conteneur parent sans toucher aux proportions internes. |
Hauteur du bloc image (HTML inline — height:100px sur la dernière bone et sur .toggle-skel-content-img) |
100 px | Modifiez les deux valeurs ensemble pour que la bone image et son équivalent réel aient la même hauteur et que le layout reste stable à la bascule. |
Couleur des bones (CSS — background: rgba(255,255,255,.08)) |
blanc à 8 % d'opacité | Adapté au fond sombre #0a0a0f. Sur fond clair, passez à rgba(0,0,0,.08) ; sur fond coloré, calquez sur la teinte dominante à 10–15 %. |
FAQ
onclick="toggleSkeleton()" résout son nom dans le scope global ; une fonction déclarée dans (function(){ … })() n'y est pas, et le clic lèverait ReferenceError: toggleSkeleton is not defined sans rien changer à la carte. Le code livré pose donc l'écouteur depuis l'intérieur de l'IIFE, avec btn.addEventListener('click', …), où card et btn sont accessibles par fermeture — c'est la convention du catalogue : vanilla, IIFE, aucune globale, donc aucun risque de collision entre deux effets copiés sur la même page. Vous pouvez tout de même exposer une fonction (window.toggleSkeleton = function(){ card.classList.toggle('loaded') } à l'intérieur de l'IIFE) si votre intégration exige un onclick, mais vous perdez cet isolement ; préférez l'écouteur tel quel, ou btn.click() depuis votre code..loaded sur #toggleSkelCard. Le plus simple : retirez le <button> du HTML (le script livré sort alors sans rien faire, vous pouvez même l'omettre) et posez la classe dans votre callback : fetch('/api/profil').then(r => r.json()).then(data => { /* injectez vos données dans .toggle-skel-content */ document.getElementById('toggleSkelCard').classList.add('loaded'); }). Préférez add à toggle : un second appel ne rebascule pas vers le squelette. Autre voie, si vous gardez le bouton masqué : document.getElementById('toggleSkelBtn').click() passe par l'écouteur livré, qui gère aussi le libellé. Dans les deux cas, le script s'exécute au chargement et cherche ses éléments immédiatement — placez-le après le HTML de la carte, ou dans DOMContentLoaded.