React accessible avec Claude Code et Radix UI
Implémentez Dialog, Dropdown et Tabs avec Claude Code et Radix UI: installation, exemples React, styles, pièges et checklist.
Créer un modal, un menu déroulant ou des onglets dans React paraît simple tant que l’on teste uniquement à la souris. Les problèmes arrivent avec le clavier: le focus ne va pas dans la fenêtre, Escape ne ferme rien, le lecteur d’écran n’annonce pas le titre, ou le focus revient au mauvais endroit après fermeture.
Radix UI est utile parce qu’il fournit des primitives sans style. Vous gardez la main sur la marque, les couleurs, l’espacement et les animations, mais vous réutilisez un comportement solide pour Dialog, Dropdown Menu et Tabs. Avec Claude Code, cette séparation évite de générer une grande quantité de logique fragile.
Le bon réflexe n’est donc pas de demander “écris-moi un modal”, mais “utilise Radix UI, garde les contraintes d’accessibilité et adapte le style au projet”. Si vous voulez une couche plus haut niveau construite sur Radix, lisez aussi le guide shadcn/ui avec Claude Code. Pour la revue globale de l’accessibilité, complétez avec le guide d’implémentation accessibilité Claude Code.
Pourquoi Radix UI convient à Claude Code
La documentation officielle présente Radix Primitives comme une bibliothèque de composants bas niveau centrée sur l’accessibilité, la personnalisation et l’expérience développeur. Elle ne cherche pas à imposer une apparence. Elle fournit les rôles, la gestion du focus, la navigation clavier et la structure des composants.
Radix Dialog, par exemple, prend en charge les modes modal et non modal, garde le focus dans le modal, ferme avec Escape et utilise Title et Description pour les annonces aux lecteurs d’écran. Radix Tabs suit le modèle WAI-ARIA des onglets avec les flèches, Home et End. Dropdown Menu gère labels, séparateurs, éléments radio et sous-menus.
Claude Code fonctionne par boucle: comprendre le contexte, modifier, vérifier, corriger. Si vous lui demandez de réinventer toutes les interactions avec des div, le résultat sera long et difficile à relire. Avec Radix UI, Claude Code peut se concentrer sur ce qui est propre au dépôt: état React, appels API, classes CSS, événements d’analytics, tests et checklist.
flowchart LR
A["Décrire le besoin à Claude Code"] --> B["Utiliser Radix UI pour le comportement"]
B --> C["Connecter état React et logique produit"]
C --> D["Appliquer CSS ou tokens du projet"]
D --> E["Vérifier clavier, lecteur d'écran et mobile"]
Installation
Radix documente aussi le paquet unifié radix-ui, mais beaucoup de projets React utilisent encore les paquets individuels. Les exemples ci-dessous utilisent cette forme pour rendre les dépendances visibles.
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tabs
Avec pnpm:
pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tabs
Si Claude Code n’est pas encore installé, consultez la documentation officielle. La voie npm ressemble à ceci:
npm install -g @anthropic-ai/claude-code
Dans une équipe, définissez aussi les règles d’autorisation, la façon de mettre à jour l’outil, les commandes permises et la responsabilité de revue. Un agent rapide sans garde-fous produit vite des diffs difficiles à valider.
Prompt à donner à Claude Code
Un prompt utile décrit le contrat d’interaction, pas seulement le rendu visuel.
claude "Ajoute à l'écran React + TypeScript existant un Dialog de confirmation, un Dropdown Menu utilisateur et des Tabs de réglages.
Contraintes:
- utiliser @radix-ui/react-dialog, @radix-ui/react-dropdown-menu et @radix-ui/react-tabs
- conserver Dialog.Title et Dialog.Description
- ajouter aria-label aux boutons de fermeture sans texte visible
- ne pas supprimer les styles de focus, utiliser :focus-visible
- réutiliser les design tokens existants si disponibles
- lister à la fin les vérifications clavier, mobile, typecheck et accessibilité"
Après la génération, relisez le diff. Vérifiez que asChild ne crée pas de bouton dans un bouton, que Dialog.Title n’a pas été supprimé pour des raisons visuelles, et que le CSS ne masque pas le focus.
Exemple React copiable
Ce fichier contient un dialogue de confirmation, un menu utilisateur et des onglets de réglages. Il fonctionne dans Vite, une SPA React ou un Client Component Next.js. Dans App Router, ajoutez "use client"; tout en haut.
import * as React from "react";
import * as Dialog from "@radix-ui/react-dialog";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import * as Tabs from "@radix-ui/react-tabs";
import "./radix-accessible-demo.css";
type User = { name: string; email: string };
type ConfirmDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
confirmLabel?: string;
danger?: boolean;
onConfirm: () => Promise<void> | void;
};
export function ConfirmDialog({
open,
onOpenChange,
title,
description,
confirmLabel = "Confirm",
danger = false,
onConfirm,
}: ConfirmDialogProps) {
const [pending, setPending] = React.useState(false);
async function handleConfirm() {
setPending(true);
try {
await onConfirm();
onOpenChange(false);
} finally {
setPending(false);
}
}
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="radix-overlay" />
<Dialog.Content className="radix-dialog">
<Dialog.Title className="radix-dialog-title">{title}</Dialog.Title>
<Dialog.Description className="radix-dialog-description">
{description}
</Dialog.Description>
<div className="button-row">
<Dialog.Close asChild>
<button type="button" className="button secondary">Cancel</button>
</Dialog.Close>
<button
type="button"
className={`button ${danger ? "danger" : "primary"}`}
disabled={pending}
onClick={handleConfirm}
>
{pending ? "Working..." : confirmLabel}
</button>
</div>
<Dialog.Close asChild>
<button type="button" className="icon-button" aria-label="Close dialog">
x
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
export function UserMenu({
user,
onOpenProfile,
onOpenBilling,
onSignOut,
}: {
user: User;
onOpenProfile: () => void;
onOpenBilling: () => void;
onSignOut: () => void;
}) {
const [theme, setTheme] = React.useState<"light" | "dark" | "system">("system");
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button type="button" className="user-trigger" aria-label={`${user.name} menu`}>
<span className="avatar" aria-hidden="true">{user.name.slice(0, 1)}</span>
<span>{user.name}</span>
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="dropdown-content" align="end" sideOffset={8}>
<DropdownMenu.Label className="dropdown-label">{user.email}</DropdownMenu.Label>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.Item className="dropdown-item" onSelect={() => onOpenProfile()}>
Profile
</DropdownMenu.Item>
<DropdownMenu.Item className="dropdown-item" onSelect={() => onOpenBilling()}>
Billing
</DropdownMenu.Item>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.RadioGroup
value={theme}
onValueChange={(value) => setTheme(value as "light" | "dark" | "system")}
>
<DropdownMenu.RadioItem className="dropdown-item" value="light">Light</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem className="dropdown-item" value="dark">Dark</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem className="dropdown-item" value="system">System</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
<DropdownMenu.Separator className="dropdown-separator" />
<DropdownMenu.Item className="dropdown-item danger-text" onSelect={() => onSignOut()}>
Sign out
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}
export function SettingsTabs() {
return (
<Tabs.Root defaultValue="profile" className="tabs-root">
<Tabs.List className="tabs-list" aria-label="Account settings">
<Tabs.Trigger className="tabs-trigger" value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger className="tabs-trigger" value="security">Security</Tabs.Trigger>
<Tabs.Trigger className="tabs-trigger" value="notifications">Notifications</Tabs.Trigger>
</Tabs.List>
<Tabs.Content className="tabs-content" value="profile">
<label className="field">
<span>Display name</span>
<input defaultValue="Masa" />
</label>
</Tabs.Content>
<Tabs.Content className="tabs-content" value="security">
<p>Require two-factor authentication before changing billing settings.</p>
<button type="button" className="button secondary">Review security</button>
</Tabs.Content>
<Tabs.Content className="tabs-content" value="notifications">
<label className="check-row">
<input type="checkbox" defaultChecked />
<span>Email me when a project is exported.</span>
</label>
</Tabs.Content>
</Tabs.Root>
);
}
Notes de style
Radix UI est sans style. Vous pouvez donc utiliser Tailwind, CSS Modules, CSS classique ou tokens. L’essentiel est de garder un focus visible, d’empêcher le Dialog de sortir de l’écran mobile et d’afficher clairement les états actifs.
.radix-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.55);
}
.radix-dialog {
position: fixed;
left: 50%;
top: 50%;
width: min(calc(100vw - 32px), 480px);
max-height: calc(100vh - 32px);
overflow: auto;
transform: translate(-50%, -50%);
border-radius: 8px;
background: white;
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.28);
padding: 24px;
}
.button-row {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.dropdown-content {
min-width: 220px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: white;
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.18);
padding: 6px;
}
.dropdown-item {
border-radius: 6px;
cursor: pointer;
outline: none;
padding: 8px 10px;
}
.dropdown-item[data-highlighted] {
background: #eff6ff;
color: #1d4ed8;
}
.tabs-list {
display: flex;
border-bottom: 1px solid #e2e8f0;
gap: 4px;
}
.tabs-trigger {
border: 0;
border-bottom: 2px solid transparent;
background: transparent;
cursor: pointer;
padding: 10px 12px;
}
.tabs-trigger[data-state="active"] {
border-color: #2563eb;
color: #1d4ed8;
font-weight: 700;
}
:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 2px;
}
Trois cas d’usage
| Cas | Apport de Radix UI | Ce que Claude Code raccorde |
|---|---|---|
| Confirmation de suppression | Focus, titre et description fiables | API, pending, erreurs, tests |
| Menu de compte | Navigation clavier, séparateurs, radio items | Utilisateur, logout, facturation, analytics |
| Tabs de réglages | Relation tablist, tab et panel stable | Formulaires, sauvegarde, URL, état modifié |
Le premier cas est un tableau de bord SaaS. Supprimer un projet, annuler un abonnement ou modifier des droits doit être clair avant d’être beau. Radix Dialog fournit le comportement; Claude Code relie l’API et le texte métier.
Le deuxième cas est un site de formation, média ou abonnement. Un menu de compte contient profil, téléchargements, historique d’achat et déconnexion. Un menu uniquement cliquable exclut vite les utilisateurs clavier.
Le troisième cas est une page de réglages. Les Tabs séparent profil, sécurité et notifications, mais chaque zone doit avoir une responsabilité de sauvegarde claire. Ajoutez cette contrainte au prompt.
Pièges fréquents
Ne supprimez pas Dialog.Title pour simplifier le visuel. Si le titre ne doit pas être visible, gardez un titre masqué visuellement. La documentation MDN du rôle dialog rappelle qu’un dialogue doit être nommé et gérer le focus correctement.
Ne cachez pas le focus. Le remplacer par un style de marque est correct; le rendre invisible ne l’est pas.
Relisez l’usage de asChild. Un bouton dans un bouton produit du HTML invalide et un comportement instable.
Évitez les piles de modales. Le modèle WAI-ARIA Modal Dialog suppose que le focus entre dans la fenêtre puis revient à l’élément déclencheur à la fermeture.
Vérifiez le mobile. Un Dialog fixe à 600px, un menu qui sort à droite ou des labels d’onglets trop longs peuvent casser une interface qui semblait propre sur desktop.
Liens officiels et checklist
- Radix Primitives Introduction
- Radix Dialog docs
- Radix Dropdown Menu docs
- Radix Tabs docs
- WAI-ARIA Modal Dialog Pattern
- MDN dialog role
- Claude Code getting started
Avant publication, ouvrez et fermez le Dialog au clavier, vérifiez Escape, le retour du focus, les flèches dans Dropdown, les flèches dans Tabs et l’absence d’overflow mobile.
CTA de monétisation
Ce sujet mène naturellement à une offre de revue d’implémentation. Le lecteur n’a pas seulement besoin de connaître Radix UI; il veut l’ajouter à un dépôt existant sans casser accessibilité, design ou conversion.
ClaudeCodeLab peut accompagner ce travail via la formation et consultation Claude Code: CLAUDE.md, règles de composants, checks d’accessibilité, prompts de revue et scripts de vérification. Pour un solo builder, proposez une checklist ou un template; pour une équipe, proposez une revue sur un écran réel.
Résultat testé
Dans un écran React de test de Masa, remplacer un modal maison par Radix Dialog et déplacer le menu et les onglets vers Radix a légèrement augmenté le code, mais a rendu la revue beaucoup plus nette. Demander à Claude Code de vérifier retour du focus, noms lus par lecteur d’écran, largeur mobile et clavier a donné des remarques plus utiles que demander seulement un meilleur rendu. Radix UI n’évite pas de penser l’accessibilité; il donne à Claude Code une base plus sûre.
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.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.