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

Créer un composant tableau React avec Claude Code : tri, filtre, pagination

Créez un tableau React avec Claude Code : tri, filtre, pagination, mobile, TanStack Table et Playwright.

Créer un composant tableau React avec Claude Code : tri, filtre, pagination

Commencer par le contrat du tableau

Un tableau n’est pas seulement un bloc visuel. Dans une application métier, il sert à comparer des clients, suivre des factures, gérer un catalogue, prioriser des articles ou lire un rapport. La première version affiche quelques lignes, puis les demandes arrivent : trier par revenu, filtrer par statut, paginer les longues listes, garder une lecture correcte sur mobile, permettre l’usage au clavier et vérifier le tout avec un test.

Claude Code est très utile pour produire ce type de composant, mais il faut lui donner un cadre précis. Une demande vague comme “fais un joli tableau” peut générer une grille en div, oublier le caption, ajouter une flèche de tri qui ne change pas les données, ou casser la lecture mobile. Une bonne demande décrit la sémantique HTML, les états, le responsive, l’accessibilité et les commandes de vérification.

Ce guide construit un composant React/TypeScript prêt à copier. Il couvre le table sémantique, les colonnes triables, le filtre global, la pagination, l’affichage mobile, l’accessibilité, le moment où TanStack Table devient utile et les vérifications Playwright. Pour le contexte React, lisez aussi développement React avec Claude Code. Pour l’accessibilité, complétez avec accessibilité avec Claude Code.

Les références officielles à garder ouvertes sont MDN <table>, MDN aria-sort, TanStack Table, Playwright Writing tests et Claude Code overview.

Les bases d’un tableau sémantique

Utilisez un tableau quand les lignes et les colonnes créent ensemble le sens. Un client avec son plan, son MRR, son statut et sa date d’inscription est une donnée tabulaire. Si les éléments sont des cartes indépendantes, une liste ou une grille de cartes peut suffire. Si l’utilisateur compare les valeurs par colonne, le table natif reste le meilleur point de départ.

La structure minimale est caption, thead, tbody et th scope="col". Le caption décrit l’objectif du tableau. Les en-têtes de colonne sont des th. Si la première cellule identifie la ligne, utilisez th scope="row".

<table>
  <caption>Revenu mensuel récurrent par client</caption>
  <thead>
    <tr>
      <th scope="col">Client</th>
      <th scope="col">Plan</th>
      <th scope="col">MRR</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Northwind</th>
      <td>Pro</td>
      <td>$1,200</td>
    </tr>
  </tbody>
</table>

Cette base est facile à négliger parce que le rendu peut ressembler à une grille classique. Pourtant, elle change tout pour les lecteurs d’écran, la navigation au clavier et les tests basés sur les rôles.

flowchart TD
  A["Exigences"] --> B["Table sémantique"]
  B --> C["Tri, filtre, pagination"]
  C --> D["Affichage mobile"]
  D --> E["Accessibilité"]
  E --> F["Tests Playwright"]

Prompt pour Claude Code

Le prompt doit limiter le périmètre, nommer les données et définir la validation. Voici un modèle utilisable dans un vrai projet.

Crée un tableau de clients en React + TypeScript.

Contraintes:
- Modifier seulement src/components/DataTable.tsx et src/components/data-table.css
- Utiliser table, caption, thead, tbody et th scope
- Champs client: id, name, plan, mrr, status, signedUpAt
- Ajouter filtre global, tri par colonne et pagination de 5 lignes
- Ajouter aria-sort uniquement sur la colonne actuellement triée
- Utiliser un button dans chaque en-tête triable
- En mobile, afficher les libellés via data-label
- Ajouter un test Playwright pour filtre, tri, pagination et mobile

Interdictions:
- Ajouter une nouvelle bibliothèque UI
- Ajouter role="grid" sans le modèle clavier correspondant
- Répondre avec du pseudocode

Ce format force Claude Code à livrer du code exécutable, pas seulement une maquette. Il protège aussi le projet contre les dépendances inutiles.

Implémentation React/TypeScript copiable

Cette version ne dépend pas d’une bibliothèque de tableau. Elle convient aux listes de petite ou moyenne taille.

// src/components/DataTable.tsx
"use client";

import { useMemo, useState, type ReactNode } from "react";
import "./data-table.css";

type SortDirection = "asc" | "desc";
type SortState<T> = { key: keyof T; direction: SortDirection } | null;

type Customer = {
  id: string;
  name: string;
  plan: "Free" | "Pro" | "Enterprise";
  mrr: number;
  status: "active" | "trial" | "paused";
  signedUpAt: string;
};

type Column<T> = {
  key: keyof T;
  label: string;
  numeric?: boolean;
  render?: (value: T[keyof T], row: T) => ReactNode;
};

const pageSize = 5;
const rows: Customer[] = [
  { id: "cus_001", name: "Northwind", plan: "Pro", mrr: 1200, status: "active", signedUpAt: "2026-01-15" },
  { id: "cus_002", name: "Blue Bottle", plan: "Free", mrr: 0, status: "trial", signedUpAt: "2026-02-02" },
  { id: "cus_003", name: "Kobayashi Studio", plan: "Enterprise", mrr: 8400, status: "active", signedUpAt: "2025-11-20" },
  { id: "cus_004", name: "Atlas Foods", plan: "Pro", mrr: 980, status: "paused", signedUpAt: "2025-12-09" },
  { id: "cus_005", name: "Green Lab", plan: "Pro", mrr: 1600, status: "active", signedUpAt: "2026-03-01" },
  { id: "cus_006", name: "Sakura Dental", plan: "Free", mrr: 0, status: "trial", signedUpAt: "2026-03-18" },
];

const money = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
});

const columns: Column<Customer>[] = [
  { key: "name", label: "Customer" },
  { key: "plan", label: "Plan" },
  { key: "mrr", label: "MRR", numeric: true, render: (_, row) => money.format(row.mrr) },
  { key: "status", label: "Status" },
  { key: "signedUpAt", label: "Signed up", render: (_, row) => new Date(row.signedUpAt).toLocaleDateString("en-US") },
];

function compare<T>(a: T, b: T, key: keyof T) {
  const left = a[key];
  const right = b[key];
  if (typeof left === "number" && typeof right === "number") return left - right;
  return String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: "base" });
}

export function DataTable() {
  const [query, setQuery] = useState("");
  const [page, setPage] = useState(1);
  const [sort, setSort] = useState<SortState<Customer>>({ key: "name", direction: "asc" });

  const filtered = useMemo(() => {
    const keyword = query.trim().toLowerCase();
    if (!keyword) return rows;
    return rows.filter((row) =>
      columns.some((column) => String(row[column.key]).toLowerCase().includes(keyword)),
    );
  }, [query]);

  const sorted = useMemo(() => {
    if (!sort) return filtered;
    return [...filtered].sort((a, b) => {
      const result = compare(a, b, sort.key);
      return sort.direction === "asc" ? result : -result;
    });
  }, [filtered, sort]);

  const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
  const currentPage = Math.min(page, totalPages);
  const pageRows = sorted.slice((currentPage - 1) * pageSize, currentPage * pageSize);

  function updateQuery(value: string) {
    setQuery(value);
    setPage(1);
  }

  function toggleSort(key: keyof Customer) {
    setSort((current) => {
      if (!current || current.key !== key) return { key, direction: "asc" };
      return { key, direction: current.direction === "asc" ? "desc" : "asc" };
    });
  }

  return (
    <section className="table-shell" aria-labelledby="customers-title">
      <label>
        <span>Filter customers</span>
        <input value={query} onChange={(event) => updateQuery(event.target.value)} type="search" />
      </label>
      <div className="table-scroll" tabIndex={0}>
        <table className="data-table">
          <caption id="customers-title">Monthly recurring revenue by customer</caption>
          <thead>
            <tr>
              {columns.map((column) => {
                const isSorted = sort?.key === column.key;
                const ariaSort = isSorted ? (sort.direction === "asc" ? "ascending" : "descending") : undefined;
                return (
                  <th key={String(column.key)} scope="col" aria-sort={ariaSort} className={column.numeric ? "numeric" : undefined}>
                    <button type="button" onClick={() => toggleSort(column.key)}>{column.label}</button>
                  </th>
                );
              })}
            </tr>
          </thead>
          <tbody>
            {pageRows.map((row) => (
              <tr key={row.id}>
                {columns.map((column, index) => {
                  const content = column.render ? column.render(row[column.key], row) : String(row[column.key]);
                  return index === 0 ? (
                    <th key={String(column.key)} scope="row" data-label={column.label}>{content}</th>
                  ) : (
                    <td key={String(column.key)} data-label={column.label} className={column.numeric ? "numeric" : undefined}>{content}</td>
                  );
                })}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <nav className="pagination" aria-label="Table pagination">
        <button type="button" disabled={currentPage === 1} onClick={() => setPage((value) => value - 1)}>Previous</button>
        <span aria-live="polite">Page {currentPage} of {totalPages}</span>
        <button type="button" disabled={currentPage === totalPages} onClick={() => setPage((value) => value + 1)}>Next</button>
      </nav>
    </section>
  );
}

La ligne la plus facile à oublier est setPage(1) dans updateQuery. Sans elle, une recherche effectuée depuis la page 2 peut afficher une page vide alors que des résultats existent.

CSS mobile et accessibilité

Le CSS suivant garde le tableau dans le DOM et transforme seulement sa présentation en dessous de 640px.

.table-scroll {
  overflow-x: auto;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
}

.data-table th,
.data-table td {
  border-top: 1px solid #e5e7eb;
  padding: 0.75rem;
  text-align: left;
}

.data-table .numeric {
  text-align: right;
}

.pagination {
  display: flex;
  gap: 0.75rem;
  justify-content: flex-end;
}

@media (max-width: 640px) {
  .data-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  .data-table,
  .data-table tbody,
  .data-table tr,
  .data-table th,
  .data-table td {
    display: block;
    width: 100%;
  }

  .data-table tr {
    border: 1px solid #d8dee8;
    border-radius: 0.5rem;
    margin-bottom: 0.75rem;
  }

  .data-table th,
  .data-table td {
    display: grid;
    grid-template-columns: 8rem 1fr;
    gap: 0.75rem;
  }

  .data-table th::before,
  .data-table td::before {
    content: attr(data-label);
    font-weight: 700;
  }
}

Vérifiez ensuite caption, scope, le bouton de tri, aria-sort, le label du filtre et aria-live pour la pagination. Évitez role="grid" si vous ne construisez pas une vraie grille interactive avec son modèle clavier.

Quand choisir TanStack Table

Un composant maison suffit pour une liste courte. TanStack Table devient intéressant avec visibilité des colonnes, filtres par colonne, sélection de lignes, pagination serveur, colonnes fixes ou virtualisation. C’est une bibliothèque headless : elle gère la logique, mais vous gardez le contrôle du HTML et du style.

OptionCas adaptéPoint de vigilance
Composant maisonPeu de colonnes, filtre et tri simplesChaque nouvelle option est à maintenir
TanStack TableÉtat complexe, serveur, sélectionAPI à apprendre et UI à construire
Grid entrepriseÉdition type tableur, gros volumesPoids, configuration, licence

Demandez à Claude Code de justifier l’ajout de dépendance. Une table de paramètres n’a pas besoin de la même architecture qu’un CRM.

Tests Playwright

Un bon test vérifie le parcours utilisateur : voir le tableau, trier, filtrer, paginer et lire le mobile.

// tests/customer-table.spec.ts
import { expect, test } from "@playwright/test";

test("customer table works", async ({ page }) => {
  await page.goto("/customers");
  await expect(page.getByRole("table", { name: /monthly recurring revenue/i })).toBeVisible();

  await page.getByRole("button", { name: /MRR/ }).click();
  await expect(page.getByRole("columnheader", { name: /MRR/ })).toHaveAttribute("aria-sort", "ascending");

  await page.getByLabel("Filter customers").fill("north");
  await expect(page.getByRole("row", { name: /Northwind/ })).toBeVisible();

  await page.getByLabel("Filter customers").fill("");
  await page.getByRole("button", { name: "Next" }).click();
  await expect(page.getByText("Page 2 of 2")).toBeVisible();

  await page.setViewportSize({ width: 390, height: 844 });
  await expect(page.locator("td[data-label='Plan']").first()).toBeVisible();
});

Donnez ce test à Claude Code lors d’une correction et demandez-lui de reproduire l’échec avant de modifier le composant.

Cas d’usage, pièges et CTA

Cas d’usageFonctions utilesValeur
Liste clients SaaSPlan, MRR, statut, renouvellementRepérer churn et upsell
Catalogue e-commerceStock, prix, catégorie, publicationDétecter erreurs et ruptures
Tableau éditorialPV, lecture, clics CTA, mise à jourPrioriser SEO et revenus
FacturationÉtat, montant, échéanceRéduire le support manuel

Les pièges fréquents sont les faux tableaux en div, les flèches de tri sans état réel, l’oubli de revenir en page 1 après filtrage, le mobile traité trop tard et l’ajout d’une bibliothèque UI inutile. Inscrivez ces points dans le prompt.

Un tableau aide aussi la monétisation quand il rend l’action suivante visible. Un média peut rapprocher trafic, clics CTA et revenus par article. Un SaaS peut rapprocher MRR, baisse d’usage et date de renouvellement. Pour mettre ce flux en place en équipe, consultez formation et accompagnement Claude Code. Pour apprendre seul, démarrez avec produits et modèles.

Résultat testé

Masa a testé cette structure sur une petite liste de clients. Le correctif le plus utile a été le retour à la page 1 après filtrage : sans cela, une recherche pouvait sembler vide. Le second gain a été d’ajouter data-label dès le début, plutôt que de bricoler le CSS mobile à la fin. Demander à Claude Code la sémantique, l’état, le responsive, l’accessibilité et Playwright dans une même tâche donne un composant plus facile à relire.

#Claude Code #table #React #TanStack Table #UI
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.