Tips & Tricks (Mis à jour: 02/06/2026)

Skeleton loading avec Claude Code : React, CLS et accessibilité

Implémentez le skeleton loading avec Claude Code : React, CLS, accessibilité, pièges et vérifications.

Skeleton loading avec Claude Code : React, CLS et accessibilité

Le skeleton loading consiste à afficher la structure approximative d’une page pendant que les données se chargent. En termes simples, on réserve la place de l’image, du titre, du résumé et des actions avant l’arrivée du vrai contenu.

Un spinner dit seulement “quelque chose est en cours”. Un skeleton indique aussi “quel type de contenu va apparaître ici”. Si une image, un emplacement publicitaire ou une réponse d’API arrive dans un espace déjà réservé, la page risque moins de sauter. Cette stabilité visuelle rejoint les recommandations de web.dev sur le Cumulative Layout Shift et les Core Web Vitals.

Ce guide montre comment demander à Claude Code une implémentation utile de skeleton loading, avec des exemples React et CSS copiables, des notes d’accessibilité, des cas d’échec et des vérifications légères. Pour compléter, consultez l’optimisation des performances, le lazy loading des images et le workflow d’accessibilité.

Découper le travail avant de coder

Un skeleton n’est pas seulement un rectangle gris. Une fonctionnalité réelle doit gérer les états chargement, succès, vide et erreur, avec une mise en page stable entre ces états. Si vous demandez seulement à Claude Code “fais un beau skeleton”, il peut produire un shimmer élégant mais oublier l’erreur, la réduction des mouvements ou les messages pour lecteurs d’écran.

flowchart LR
  P["Prompt Claude Code"] --> S["skeleton de taille proche"]
  S --> D["données chargées"]
  D --> E["état vide"]
  D --> X["état d'erreur"]
  S --> A["aria-busy / status"]
  S --> M["prefers-reduced-motion"]
  S --> C["contrôle CLS"]

Commencez par un prompt qui fixe le périmètre et la vérification :

Read the existing card/list components before editing.
Implement skeleton loading only for the article cards list.
Keep the skeleton dimensions close to the loaded content.
Handle loading, empty, error, and success states.
Respect prefers-reduced-motion and avoid layout shift.
Add a small Playwright check if the project already uses Playwright.
Do not change unrelated styles, routing, or data fetching.

prefers-reduced-motion est une condition CSS qui indique si l’utilisateur a demandé moins de mouvement dans son système ou son navigateur. Les effets shimmer trop présents peuvent gêner certaines personnes. Il faut donc vérifier la référence MDN sur prefers-reduced-motion et prévoir une version statique.

Cas d’usage concrets

Le skeleton loading est particulièrement utile quand l’utilisateur comprend ce qui va arriver, mais que les données ne sont pas encore prêtes.

Cas d’usageCe qu’il faut réserverPoint de vigilance
Liste de cartes d’articlesVignette, titre sur deux lignes, résumé, tagGarder une hauteur média fixe pour éviter les sauts
DashboardCartes KPI, cadres de graphiques, activité récenteNe pas afficher des chiffres partiels trompeurs
Grille e-commerceImage, nom, prix, noteNe pas laisser croire à un prix ou stock obsolète
Table d’administrationEn-tête, lignes, zone d’actionsSi le nombre de lignes change beaucoup, revoir aussi pagination et filtres
Landing page de conseilPreuves, CTA, FAQUn CTA tardif peut repousser le chemin de conversion

Dans un tunnel de consultation ClaudeCodeLab, le gain le plus net est venu de la réservation de la hauteur des cartes et du CTA avant l’arrivée des données. L’erreur consistait à faire un skeleton plus haut que le contenu final : pendant le chargement tout semblait calme, puis la page remontait. Un skeleton n’est pas une décoration, c’est un contrat de mise en page.

Exemple React copiable

Collez cet exemple dans src/App.tsx d’un projet Vite + React + TypeScript. Il simule la latence avec setTimeout et permet de basculer entre succès, vide et erreur. C’est aussi le niveau de détail que je donne à Claude Code avant de le laisser modifier un vrai dépôt.

import { useEffect, useState } from "react";
import "./skeleton-demo.css";

type Article = {
  id: number;
  title: string;
  description: string;
  tag: string;
};

type LoadState = "loading" | "success" | "empty" | "error";

const demoArticles: Article[] = [
  {
    id: 1,
    title: "Créer des diff UI plus sûrs avec Claude Code",
    description: "Lire les composants existants puis améliorer seulement l'expérience de chargement.",
    tag: "UX",
  },
  {
    id: 2,
    title: "Réserver l'espace image sans augmenter le CLS",
    description: "Fixer les hauteurs média, titre et résumé avant l'arrivée des données.",
    tag: "Performance",
  },
  {
    id: 3,
    title: "Rendre les états de chargement accessibles",
    description: "Combiner aria-busy, messages status et réduction du mouvement.",
    tag: "A11y",
  },
];

function SkeletonLine({ width = "100%" }: { width?: string }) {
  return <span className="sk-line" style={{ width }} aria-hidden="true" />;
}

function ArticleCardSkeleton() {
  return (
    <article className="article-card is-skeleton" aria-hidden="true">
      <div className="sk-media" />
      <div className="article-card__body">
        <SkeletonLine width="46%" />
        <SkeletonLine />
        <SkeletonLine width="86%" />
        <SkeletonLine width="32%" />
      </div>
    </article>
  );
}

function ArticleCard({ article }: { article: Article }) {
  return (
    <article className="article-card">
      <div className="article-card__media">{article.tag}</div>
      <div className="article-card__body">
        <p className="article-card__tag">{article.tag}</p>
        <h2>{article.title}</h2>
        <p>{article.description}</p>
      </div>
    </article>
  );
}

export default function App() {
  const [state, setState] = useState<LoadState>("loading");
  const [articles, setArticles] = useState<Article[]>([]);

  useEffect(() => {
    const timer = window.setTimeout(() => {
      setArticles(demoArticles);
      setState("success");
    }, 1200);

    return () => window.clearTimeout(timer);
  }, []);

  const reloadAs = (nextState: LoadState) => {
    setState("loading");
    setArticles([]);

    window.setTimeout(() => {
      setArticles(nextState === "success" ? demoArticles : []);
      setState(nextState);
    }, 700);
  };

  return (
    <main className="demo-shell">
      <div className="demo-toolbar" aria-label="Changer l'état visible">
        <button onClick={() => reloadAs("success")}>Succès</button>
        <button onClick={() => reloadAs("empty")}>Vide</button>
        <button onClick={() => reloadAs("error")}>Erreur</button>
      </div>

      <section
        aria-busy={state === "loading"}
        aria-describedby="article-list-status"
        className="article-grid"
      >
        <p id="article-list-status" className="sr-only" role="status">
          {state === "loading" ? "Chargement de la liste d'articles" : "Liste d'articles chargée"}
        </p>

        {state === "loading" &&
          Array.from({ length: 3 }).map((_, index) => (
            <ArticleCardSkeleton key={index} />
          ))}

        {state === "success" &&
          articles.map((article) => (
            <ArticleCard key={article.id} article={article} />
          ))}

        {state === "empty" && (
          <div className="state-panel">Aucun article à afficher pour le moment.</div>
        )}

        {state === "error" && (
          <div className="state-panel" role="alert">
            Impossible de charger la liste d'articles. Réessayez plus tard.
          </div>
        )}
      </section>
    </main>
  );
}

Le point d’accessibilité important est de ne pas faire annoncer chaque forme du skeleton. Un lecteur d’écran n’a pas besoin d’entendre qu’il y a trois barres grises. Ici les cartes skeleton sont enaria-hidden, tandis qu’un seulrole="status" décrit l’état de la liste. Consultez ARIA status role sur MDN pour les mises à jour non urgentes.

CSS pour stabiliser taille et mouvement

Enregistrez ce CSS danssrc/skeleton-demo.css. L’essentiel est de garder lemin-height, la hauteur média et l’espacement proches entre l’état de chargement et l’état final. Le shimmer reste discret et s’arrête pour les utilisateurs qui préfèrent moins de mouvement.

:root {
  color: #18212f;
  background: #f6f7f9;
  font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

button {
  min-height: 40px;
  border: 1px solid #b8c2d6;
  border-radius: 8px;
  background: #ffffff;
  color: #18212f;
  padding: 0 14px;
  font-weight: 700;
}

.demo-shell {
  width: min(1040px, calc(100% - 32px));
  margin: 40px auto;
}

.demo-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 18px;
}

.article-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
}

.article-card {
  min-height: 316px;
  overflow: hidden;
  border: 1px solid #d7deea;
  border-radius: 8px;
  background: #ffffff;
}

.article-card__media,
.sk-media {
  display: grid;
  min-height: 148px;
  place-items: center;
  background: #dfe7f3;
  color: #39506f;
  font-weight: 800;
}

.article-card__body {
  display: grid;
  gap: 10px;
  padding: 18px;
}

.article-card__tag {
  color: #3b6b4f;
  font-size: 0.875rem;
  font-weight: 800;
}

.article-card h2 {
  min-height: 56px;
  margin: 0;
  font-size: 1.16rem;
  line-height: 1.45;
}

.article-card p {
  margin: 0;
  line-height: 1.7;
}

.sk-line,
.sk-media {
  border-radius: 8px;
  background: linear-gradient(90deg, #d9e0ea 25%, #edf1f7 37%, #d9e0ea 63%);
  background-size: 240% 100%;
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
}

.sk-line {
  display: block;
  height: 16px;
}

.state-panel {
  min-height: 180px;
  display: grid;
  place-items: center;
  border: 1px solid #d7deea;
  border-radius: 8px;
  background: #ffffff;
  padding: 24px;
  text-align: center;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

@keyframes skeleton-shimmer {
  from {
    background-position: 120% 0;
  }

  to {
    background-position: -120% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sk-line,
  .sk-media {
    animation: none;
    background: #d9e0ea;
  }
}

Ce CSS privilégie la stabilité plutôt qu’un effet spectaculaire. Si les largeurs des lignes skeleton sont trop aléatoires, le contenu final semble appartenir à un autre design. En pratique, il vaut mieux réserver un titre de deux lignes, un résumé de deux lignes et une zone média prévisible.

Vérification Playwright minimale

Si le projet utilise déjà Playwright, ajoutez une vérification ciblée. Elle ne prouve pas que le CLS réel est parfait, mais elle repère les régressions évidentes avant la revue.

import { expect, test } from "@playwright/test";

test("article skeleton keeps a stable card area", async ({ page }) => {
  await page.goto("/");

  await expect(page.getByText("Chargement de la liste d'articles")).toBeAttached();
  await expect(page.locator(".is-skeleton")).toHaveCount(3);

  const firstBox = await page.locator(".article-card").first().boundingBox();
  expect(firstBox?.height).toBeGreaterThan(280);

  await page.getByRole("button", { name: "Erreur" }).click();
  await expect(page.getByRole("alert")).toContainText("Impossible de charger");
});

Le CLS réel dépend aussi des images, publicités, polices, scripts tiers, conditions réseau et appareils. Cette vérification sert d’alerte précoce ; les données de production et la documentation web.dev restent nécessaires pour juger sérieusement.

Pièges fréquents

Le premier piège est un skeleton qui ne correspond pas au contenu final. Il paraît propre pendant le chargement, puis la vraie carte s’étire ou se contracte. Réservez les dimensions d’image avecwidth, height ouaspect-ratio, et prévoyez aussi l’espace des publicités et CTA.

Le deuxième piège consiste à afficher un skeleton pour toutes les requêtes rapides. Si la requête finit souvent en 100 ms, cela ajoute du scintillement. En production, utilisez des règles comme “afficher après 300 ms”, “seulement au premier chargement” ou “conserver les anciennes données pendant le rafraîchissement”.

Le troisième piège est le bruit d’accessibilité. Unrole="status" par carte peut provoquer des annonces répétées. Gardez le message live au niveau de la liste et masquez les formes purement visuelles.

Le quatrième piège est l’oubli du chemin d’erreur. Si l’API échoue et que le skeleton reste affiché, l’utilisateur croit que la page charge encore. Concevez les états erreur, vide et nouvel essai séparément.

Le cinquième piège est de laisser Claude Code décider la priorité produit. Claude Code peut lire les fichiers et générer du code, mais l’humain décide quel CTA doit rester visible, combien d’espace réserver à la publicité et quel contenu peut apparaître en premier.

Prompt de revue pour Claude Code

Après l’implémentation, passez en mode revue :

Review only the skeleton loading changes.
Check whether loaded content and skeleton content reserve similar space.
Check loading, success, empty, and error states.
Check reduced-motion behavior and ARIA announcements.
Point out any code that may increase CLS or create repeated screen reader messages.
Return findings with file names and exact lines.

Ce prompt transforme Claude Code d’implémenteur en critique. Dans un site éditorial, publicités, articles liés, CTA et images lazy affectent le même flux visuel. Utilisez le guide CSS et les stratégies de test pour séparer contrôle visuel, lecture assistive et tests de régression.

Lien avec la monétisation

Le skeleton loading protège aussi les parcours de revenus. Si un CTA de conseil, une carte produit, un formulaire newsletter ou une publicité apparaît tard et pousse l’article vers le bas, le lecteur perd sa position ou clique au mauvais endroit. Cela nuit à la confiance et à la conversion.

Pour un usage individuel, commencez par la cheatsheet gratuite Claude Code afin de stabiliser vos étapes de revue. Pour une équipe qui veut intégrer skeleton loading, lazy images, Core Web Vitals et accessibilité dans un workflow commun, utilisez la formation et consultation Claude Code. Masa peut partir du dépôt existant et transformer les améliorations en prompts, composants et contrôles concrets.

Résumé

Un bon skeleton loading n’est pas un artifice pour cacher l’attente. Il réserve un espace proche de l’écran final, réduit l’incertitude et limite les mouvements visibles. Avec Claude Code, fournissez en même temps le composant cible, les états, les dimensions, les exigences d’accessibilité et les commandes de vérification.

Après avoir testé ce workflow, les changements les plus utiles ont été de fixer d’abord la hauteur des médias et des titres, d’annoncer le chargement au niveau de la liste, et d’arrêter l’animation pour les utilisateurs qui réduisent le mouvement. Quand je me suis concentré seulement sur le shimmer, les états vide et erreur ont été repoussés et la revue a pris plus de temps. Demandez l’implémentation et la vérification ensemble.

#Claude Code #skeleton loading #React #UX #accessibilité
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.