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

Scroll virtuel React avec Claude Code et TanStack Virtual

Implémentez un scroll virtuel avec Claude Code, TanStack Virtual, hauteurs variables, accessibilité et Playwright.

Scroll virtuel React avec Claude Code et TanStack Virtual

Pourquoi virtualiser une liste

Le scroll virtuel consiste à ne rendre dans le DOM que les lignes visibles et quelques lignes autour de la zone de scroll. Le DOM est l’arbre d’éléments que le navigateur utilise pour calculer la mise en page, peindre l’écran, gérer les événements et exposer l’interface aux technologies d’assistance. Quand une liste contient plusieurs milliers d’éléments, un simpleitems.map(...) finit par faire travailler le navigateur sur des lignes que l’utilisateur ne voit jamais.

Claude Code sait produire très vite un composant React, mais une demande trop vague comme “ajoute du virtual scroll” donne souvent une démo fragile. En production, il faut préciser la hauteur des lignes, le comportement au clavier, la restauration de position, la largeur mobile, les images chargées tardivement, les attributs d’accessibilité et les tests qui prouvent que la fonctionnalité tient.

Les cas d’usage typiques sont un viewer de logs, une liste de clients, un historique de chat, des résultats de recherche et une table d’administration. Les logs exigent un scroll rapide sur des milliers de lignes. Une liste CRM doit garder la sélection et revenir au bon endroit après une fiche détail. Un chat mélange texte long, avatars et pièces jointes. Les résultats de recherche se recalculent à chaque filtre. Les tables back-office combinent colonnes, actions, rôles et états. Si votre problème principal est le chargement progressif de données serveur, lisez aussi infinite scroll. Pour le contexte global, gardez l’optimisation des performances sous la main.

Le prompt à donner à Claude Code

Le bon prompt protège la fonctionnalité. Il ne demande pas seulement de la vitesse ; il fixe les contraintes de comportement, d’accessibilité et de preuve.

Implémente un viewer de logs virtualisé en React 18 + TypeScript.

Exigences:
- Utiliser @tanstack/react-virtual.
- Supporter plus de 10000 lignes sans toutes les monter dans le DOM.
- Utiliser une hauteur de ligne par défaut de 44px.
- Ajouter role, aria-label, aria-posinset et aria-setsize.
- Garder une largeur correcte à 390px sans overflow horizontal de page.
- Expliquer le choix d'overscan.
- Ajouter un test Playwright pour le scroll et la largeur mobile.
- Relire le code avec la documentation officielle TanStack Virtual.

Cette forme aide Claude Code à produire un composant vérifiable. Pour une liste de clients, remplacez les champs par le nom, le plan, le statut et la date de dernier contact. Pour une recherche, utilisez titre, résumé et tags. Pour un chat, utilisez auteur, message et média. Le principe reste le même : commencer par les risques réels, pas par un composant générique.

Liste à hauteur fixe avec TanStack Virtual

@tanstack/react-virtual est une base solide pour React. C’est une librairie headless : elle calcule les éléments virtuels, les offsets et la taille totale, mais elle ne fournit pas votre UI. Vous gardez donc la main sur le HTML, les styles et l’accessibilité. Référez-vous à la documentation officielle TanStack Virtual et à la Virtualizer API.

npm install @tanstack/react-virtual

Voici un viewer de logs à hauteur fixe. Le conteneur externe scrolle, le conteneur interne donne la hauteur totale, et les lignes visibles sont positionnées avectranslateY.

import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";

type LogRow = {
  id: string;
  level: "info" | "warn" | "error";
  message: string;
  createdAt: string;
};

export function VirtualLogViewer({ rows }: { rows: LogRow[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const rowVirtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 44,
    overscan: 12,
    getItemKey: (index) => rows[index]?.id ?? index,
  });

  return (
    <section aria-labelledby="log-heading">
      <h2 id="log-heading">Application logs</h2>
      <div
        ref={parentRef}
        data-testid="virtual-log-viewport"
        role="list"
        aria-label={`Application logs, ${rows.length} rows`}
        style={{
          height: 520,
          overflow: "auto",
          border: "1px solid #d4d4d8",
          borderRadius: 6,
        }}
      >
        <div
          style={{
            height: rowVirtualizer.getTotalSize(),
            position: "relative",
            width: "100%",
          }}
        >
          {rowVirtualizer.getVirtualItems().map((virtualRow) => {
            const row = rows[virtualRow.index];
            if (!row) return null;

            return (
              <div
                key={virtualRow.key}
                role="listitem"
                aria-posinset={virtualRow.index + 1}
                aria-setsize={rows.length}
                style={{
                  position: "absolute",
                  top: 0,
                  left: 0,
                  width: "100%",
                  height: `${virtualRow.size}px`,
                  transform: `translateY(${virtualRow.start}px)`,
                  display: "grid",
                  gridTemplateColumns: "92px 72px minmax(0, 1fr)",
                  gap: 12,
                  alignItems: "center",
                  padding: "0 12px",
                  boxSizing: "border-box",
                  borderBottom: "1px solid #eee",
                }}
              >
                <time dateTime={row.createdAt}>{row.createdAt}</time>
                <strong>{row.level.toUpperCase()}</strong>
                <span style={{ overflowWrap: "anywhere" }}>{row.message}</span>
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

overscan indique combien de lignes supplémentaires sont rendues hors écran. Trop peu d’overscan peut afficher des blancs lors d’un scroll rapide. Trop d’overscan remonte le nombre de nœuds DOM et annule une partie du gain. Pour des logs texte, testez entre 8 et 16. Pour des lignes lourdes avec menus, avatars, graphiques ou coloration syntaxique, commencez plus bas et mesurez.

Historique de chat à hauteur variable

Les chats, commentaires de support et réponses IA ont des hauteurs variables. Le texte, les images, les pièces jointes, les traductions et les messages d’erreur changent la taille réelle. Dans ce cas, donnez une estimation puis mesurez l’élément rendu.

import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";

type Message = {
  id: string;
  author: string;
  body: string;
  avatarUrl?: string;
};

export function VirtualChatHistory({ messages }: { messages: Message[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: messages.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 96,
    overscan: 8,
    getItemKey: (index) => messages[index]?.id ?? index,
  });

  return (
    <div
      ref={parentRef}
      role="log"
      aria-label="Chat history"
      style={{ height: 520, overflow: "auto" }}
    >
      <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
        {virtualizer.getVirtualItems().map((virtualItem) => {
          const message = messages[virtualItem.index];
          if (!message) return null;

          return (
            <article
              key={virtualItem.key}
              data-index={virtualItem.index}
              ref={virtualizer.measureElement}
              style={{
                position: "absolute",
                top: 0,
                left: 0,
                width: "100%",
                transform: `translateY(${virtualItem.start}px)`,
                padding: "12px 16px",
                boxSizing: "border-box",
              }}
            >
              {message.avatarUrl ? (
                <img
                  src={message.avatarUrl}
                  alt=""
                  width={32}
                  height={32}
                  loading="lazy"
                  onLoad={() => virtualizer.measure()}
                />
              ) : null}
              <p style={{ margin: 0, fontWeight: 700 }}>{message.author}</p>
              <p style={{ margin: "4px 0 0", overflowWrap: "anywhere" }}>
                {message.body}
              </p>
            </article>
          );
        })}
      </div>
    </div>
  );
}

Les images chargées après coup provoquent souvent un saut de scroll. Réservez largeur et hauteur, puis relancez une mesure après le chargement. Pour un chat, décidez aussi si les nouveaux messages doivent coller la vue au bas de la conversation ou respecter la position d’un utilisateur qui lit l’historique.

Accessibilité, clavier et restauration

Une liste virtualisée ne contient pas tout dans le DOM. Elle doit donc mieux expliquer son rôle, le nombre total, la position courante et les raccourcis clavier. Une liste de clients peut utiliser les flèches, PageUp, PageDown, Home et End.

import type { KeyboardEvent } from "react";

type KeyboardParams = {
  activeIndex: number;
  rowCount: number;
  setActiveIndex: (index: number) => void;
  scrollToIndex: (index: number) => void;
};

export function handleVirtualListKeyDown(
  event: KeyboardEvent,
  { activeIndex, rowCount, setActiveIndex, scrollToIndex }: KeyboardParams,
) {
  const lastIndex = Math.max(0, rowCount - 1);
  let nextIndex = activeIndex;

  if (event.key === "ArrowDown") nextIndex = Math.min(lastIndex, activeIndex + 1);
  if (event.key === "ArrowUp") nextIndex = Math.max(0, activeIndex - 1);
  if (event.key === "PageDown") nextIndex = Math.min(lastIndex, activeIndex + 10);
  if (event.key === "PageUp") nextIndex = Math.max(0, activeIndex - 10);
  if (event.key === "Home") nextIndex = 0;
  if (event.key === "End") nextIndex = lastIndex;

  if (nextIndex !== activeIndex) {
    event.preventDefault();
    setActiveIndex(nextIndex);
    scrollToIndex(nextIndex);
  }
}

Évitez de dépendre uniquement du focus placé sur une ligne qui peut être démontée. Le conteneur peut garder le focus et exposer la ligne active avecaria-activedescendant. Après une fiche détail, restaurez la position avec une clé qui inclut les filtres et le tri. Pour compléter, lisez le guide accessibilité.

Test Playwright et prompt de revue

Vérifiez au minimum le scroll vers une ligne connue, la largeur mobile, l’absence d’overflow horizontal et les erreurs de console.

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

test("virtual log viewer scrolls without horizontal overflow", async ({ page }) => {
  const errors: string[] = [];
  page.on("console", (message) => {
    if (message.type() === "error") errors.push(message.text());
  });

  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto("/debug/virtual-log-viewer");

  const viewport = page.getByTestId("virtual-log-viewport");
  await expect(viewport).toBeVisible();

  const before = await viewport.boundingBox();
  await viewport.evaluate((node) => {
    node.scrollTop = 2400;
  });
  await expect(page.getByText("Log #250")).toBeVisible();
  const after = await viewport.boundingBox();

  expect(after?.width).toBe(before?.width);
  expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(
    await page.evaluate(() => document.documentElement.clientWidth),
  );
  expect(errors).toEqual([]);
});

Demandez ensuite une revue critique à Claude Code.

Relis cette implementation de scroll virtuel React.

Verifie:
- L'usage de l'API officielle TanStack Virtual.
- La separation entre hauteur fixe et hauteur variable.
- Le risque de blancs si overscan est trop faible.
- La coherence des role, aria et controles clavier.
- La mesure apres chargement des images.
- La restauration de position apres retour depuis une page detail.
- Le risque SSR ou hydration sur la hauteur initiale.
- La presence d'un test Playwright mobile et scroll.

Pièges, CTA et vérification

Les pièges récurrents sont concrets : traiter des lignes variables comme fixes, régler overscan trop bas ou trop haut, oublier le clavier, ne pas annoncer le total aux lecteurs d’écran, perdre la position après une page détail, laisser les images changer la hauteur, subir un décalage SSR, ou casser la largeur mobile avec une chaîne trop longue. Le modèle mental utile estscrollTop -> plage visible -> overscan -> lignes virtuelles -> translateY -> mesure réelle.

Pour appliquer cela à un vrai outil interne, la page formation et conseil Claude Code peut aider à transformer l’implémentation en méthode d’équipe : prompt, règlesCLAUDE.md, revue accessibilité et preuve Playwright. Références utiles : TanStack Virtual docs, infinite scroll et performance optimization.

Après test, le viewer de logs à hauteur fixe a fortement réduit le nombre de lignes montées par rapport à unrows.map complet. Le chat à hauteur variable a montré le vrai risque : sans dimensions réservées et nouvelle mesure après chargement d’image, le scroll bouge légèrement. La checklist la plus utile est devenue : réglerestimateSize avec de vraies données, tester 390px, scroller vers une ligne connue au milieu et vérifier l’absence d’overflow horizontal.

#Claude Code #scroll virtuel #performance #React #windowing
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.