D
design.md
activeConventions UI communes à tous nos projets : stack, primitives, patterns custom, accessibilité minimale, copy.
standard · mise à jour 10 juin 2026
designuitailwindaccessibilityconventions
Design : cross-project conventions
Applies to every project. Project-specific rules (color tokens, typography, density, animations, i18n) belong in the design-system.md at the project root.
Universal UI stack
- Tailwind CSS (v3 or v4 depending on the project, never mixed within one project)
- Radix UI + shadcn/ui : primitive library, never replaced by Ant Design, MUI, Chakra
- CVA (
class-variance-authority) : component variants - tailwind-merge : conflict-free class merging
cn()utility :clsx+tailwind-merge, exported fromsrc/lib/utils.ts- Lucide React : icons (not heroicons, not react-icons)
Component organization
src/components/
ui/ ← shadcn primitives (atoms)
design/ ← reusable domain components (molecules)
layout/ ← Sidebar, Topbar, Navbar, Footer
[domain]/ ← feature-specific components
- PascalCase for files:
FormField.tsx,StatusBadge.tsx - Named export +
displayNameforforwardRefcomponents - Props type:
${ComponentName}Props(e.g.ButtonProps)
shadcn primitives (always present)
Atoms: Button, Input, Textarea, Label, Checkbox, Switch, Select, Badge, Avatar, Skeleton
Molecules: Card, Dialog, DropdownMenu, Tabs, Tooltip, Popover, RadioGroup
Never rebuild these from scratch. Extend them via CVA or wrapping when needed.
Recurring custom patterns
Combobox
- Built on cmdk (
Command+CommandInput+CommandList), not Radix Select - Live search on keystroke, no submit button
- Clear button (× icon,
size-3) on the right,stopPropagation - Dropdown chevron on the right,
text-gray-400 size-3.5 - Search placeholder:
"Rechercher…" - Empty state:
"Aucun X trouvé" - Max 50 results shown (filter/paginate beyond that)
- Keyboard: arrows to navigate, Enter to select, Escape to close
FormField
<div className="flex flex-col gap-1.5">
<Label htmlFor={id}>{label}</Label>
<Input id={id} aria-invalid={!!error} aria-describedby={error ? errorId : undefined} />
{error && <p id={errorId} className="text-xs text-danger" role="alert">{error}</p>}
{helpText && !error && <p className="text-xs text-gray-500">{helpText}</p>}
</div>
DataTable
- Skeleton rows (
h-4 w-full) while loading - Empty state:
"Aucun résultat", centered - Columns via
render: (row) => ReactNodecallback - Semantic structure:
<Table>,<TableHeader>,<TableBody>,<TableRow>,<TableCell>
StatusBadge
outlinevariant with semantic colors- Colors: success (green), warning (amber), danger (red), info (blue), neutral (gray)
- Labels in French: "Actif", "En attente", "Erreur", "Inactif", etc.
Accessibility (non-negotiable minimum)
- Inputs:
aria-invalid={!!error},aria-describedbypointing to the error message ID - Errors:
role="alert"on the message, automatic announcement - Visible focus:
focus-visible:ring-2 focus-visible:ring-offset-1, never removeoutline - Disabled:
disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none - Decorative icons: no
aria-label(Lucide renders them decorative by default) - Meaningful icons (no adjacent text):
aria-labelrequired - Modals: trap focus (Radix Dialog handles it), Escape to close
- Labels: always
htmlForbound to the inputid, never a visual label without a link
UI copy (French by default)
- Empty states:
"Aucun X trouvé"/"Aucun résultat" - Actions:
"Ajouter","Supprimer","Enregistrer","Annuler","Modifier" - Destructive confirmation:
"Supprimer définitivement"(not just"Supprimer") - Search placeholder:
"Rechercher…" - Loading:
"Chargement…"(not"Loading...") - No
—in visible text (see global CLAUDE.md rule)
What does NOT belong here (→ project design-system.md)
- Color tokens (primary palette, semantic, dark mode)
- Typography (font choice, base size, weights)
- Density (compact back-office 14px vs relaxed marketing 16px)
- Custom animations and micro-interactions
- i18n approach and routing conventions
- Breakpoints and specific layout max-width