Aller au contenu

Composantes UI : shadcn/ui

shadcn/ui est une collection de composantes réutilisables et accessibles construites avec Radix UI et Tailwind CSS. Contrairement aux librairies de composantes traditionnelles, shadcn/ui ne s'installe pas comme un package npm — les composantes sont copiées directement dans votre projet, ce qui vous donne un contrôle total sur leur code source.

Avantage Description
Code dans votre projet Les composantes font partie de votre code source, vous pouvez les modifier librement
Accessibilité (ARIA) Construit sur Radix UI, chaque composante respecte les standards d'accessibilité
Tailwind CSS natif Les styles utilisent vos classes Tailwind et s'adaptent à votre thème
TypeScript Toutes les composantes sont typées

Prérequis

shadcn/ui nécessite Tailwind CSS. Assurez-vous d'avoir suivi la leçon sur Tailwind CSS avant de continuer.

Installation de shadcn

1. Modifier tsconfig.json

shadcn/ui utilise l'alias @/ pour importer ses composantes. Il faut configurer TypeScript pour reconnaître cet alias.

Modifiez tsconfig.json à la racine du projet :

tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" }
  ],
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Modifiez aussi tsconfig.app.json pour ajouter les mêmes chemins dans les options du compilateur :

tsconfig.app.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

2. Modifier vite.config.ts

Ajoutez la résolution de l'alias @/ dans la configuration Vite :

vite.config.ts
import path from "path"
import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})

Installez ensuite le module Node pour la résolution de chemins :

console
npm install -D @types/node

3. Initialiser shadcn/ui

console
npx shadcn@latest init

L'assistant interactif vous posera quelques questions. Voici les choix recommandés :

console
? Select a component library  - Use arrow-keys. Return to submit.
   Radix
    Base
? Which preset would you like to use?  - Use arrow-keys. Return to submit.
   Nova - Lucide / Geist
    Vega
    Maia
    Lyra
    Mira
    Luma
    Sera
    Rhea
    Custom

Cette commande crée un fichier components.json de configuration et ajoute les styles de base dans votre fichier CSS.

4. Ajouter des composantes

Chaque composante s'installe séparément avec la commande add. Le code source de la composante est copié dans src/components/ui/ :

console
npx shadcn@latest add button
npx shadcn@latest add navigation-menu
npx shadcn@latest add field
npx shadcn@latest add input
npx shadcn@latest add card
npx shadcn@latest add badge

Tip

Vous pouvez ajouter plusieurs composantes d'un seul coup :

console
npx shadcn@latest add button card badge input field navigation-menu

Barre de navigation

La composante NavigationMenu permet de créer une barre de navigation avec des menus déroulants accessibles.

src/components/NavBar/NavBar.tsx
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import { cn } from "@/lib/utils"

export function NavBar() {
  return (
    <header className="border-b px-6 py-3 flex items-center justify-between">
      <div className="font-bold text-xl">Ma Boutique</div>

      <NavigationMenu>
        <NavigationMenuList>

          {/* Lien simple */}
          <NavigationMenuItem>
            <NavigationMenuLink className={navigationMenuTriggerStyle()} href="/">
              Accueil
            </NavigationMenuLink>
          </NavigationMenuItem>

          {/* Menu déroulant */}
          <NavigationMenuItem>
            <NavigationMenuTrigger>Produits</NavigationMenuTrigger>
            <NavigationMenuContent>
              <ul className="grid gap-3 p-4 w-[300px]">
                <li>
                  <NavigationMenuLink asChild>
                    <a
                      href="/electronique"
                      className={cn("block p-3 rounded-md hover:bg-accent")}
                    >
                      <div className="font-medium">Électronique</div>
                      <p className="text-sm text-muted-foreground">
                        Téléphones, tablettes et plus
                      </p>
                    </a>
                  </NavigationMenuLink>
                </li>
                <li>
                  <NavigationMenuLink asChild>
                    <a
                      href="/vetements"
                      className={cn("block p-3 rounded-md hover:bg-accent")}
                    >
                      <div className="font-medium">Vêtements</div>
                      <p className="text-sm text-muted-foreground">
                        Mode homme et femme
                      </p>
                    </a>
                  </NavigationMenuLink>
                </li>
              </ul>
            </NavigationMenuContent>
          </NavigationMenuItem>

          {/* Lien simple */}
          <NavigationMenuItem>
            <NavigationMenuLink className={navigationMenuTriggerStyle()} href="/contact">
              Contact
            </NavigationMenuLink>
          </NavigationMenuItem>

        </NavigationMenuList>
      </NavigationMenu>
    </header>
  )
}

Éléments clés de NavigationMenu

Composante Rôle
NavigationMenu Conteneur principal
NavigationMenuList Liste des éléments du menu
NavigationMenuItem Un élément du menu
NavigationMenuTrigger Bouton qui ouvre un sous-menu
NavigationMenuContent Contenu du sous-menu déroulant
NavigationMenuLink Lien de navigation
navigationMenuTriggerStyle() Fonction qui retourne les classes de style d'un bouton de navigation

Formulaire

shadcn/ui intègre react-hook-form pour la gestion des formulaires et Zod pour la validation des données.

Installer les dépendances

console
npm install react-hook-form zod @hookform/resolvers

Ajouter les composantes shadcn/ui nécessaires :

console
npx shadcn@latest add field input button

Exemple de formulaire de contact

src/components/FormulaireContact/FormulaireContact.tsx
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { Button } from "@/components/ui/button"
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

// 1. Définir le schéma de validation avec Zod
const schemaFormulaire = z.object({
  nom: z.string().min(2, "Le nom doit contenir au moins 2 caractères."),
  courriel: z.string().email("Adresse courriel invalide."),
  message: z.string().min(10, "Le message doit contenir au moins 10 caractères."),
})

// 2. Dériver le type TypeScript depuis le schéma
type DonneesFormulaire = z.infer<typeof schemaFormulaire>

export function FormulaireContact() {
  // 3. Initialiser react-hook-form avec le résolveur Zod
  const form = useForm<DonneesFormulaire>({
    resolver: zodResolver(schemaFormulaire),
    defaultValues: {
      nom: "",
      courriel: "",
      message: "",
    },
  })

  // 4. Gérer la soumission (appelée seulement si la validation passe)
  function onSubmit(donnees: DonneesFormulaire) {
    console.log("Formulaire soumis :", donnees)
  }

  return (
    <div className="max-w-md mx-auto p-6">
      <h2 className="text-2xl font-bold mb-6">Contactez-nous</h2>

      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">

        <Controller
          name="nom"
          control={form.control}
          render={({ field, fieldState }) => (
            <Field data-invalid={fieldState.invalid}>
              <FieldLabel htmlFor={field.name}>Nom</FieldLabel>
              <Input
                {...field}
                id={field.name}
                aria-invalid={fieldState.invalid}
                placeholder="Jean Tremblay"
              />
              {fieldState.invalid && (
                <FieldError errors={[fieldState.error]} />
              )}
            </Field>
          )}
        />

        <Controller
          name="courriel"
          control={form.control}
          render={({ field, fieldState }) => (
            <Field data-invalid={fieldState.invalid}>
              <FieldLabel htmlFor={field.name}>Courriel</FieldLabel>
              <Input
                {...field}
                id={field.name}
                type="email"
                aria-invalid={fieldState.invalid}
                placeholder="jean@example.com"
              />
              {fieldState.invalid && (
                <FieldError errors={[fieldState.error]} />
              )}
            </Field>
          )}
        />

        <Controller
          name="message"
          control={form.control}
          render={({ field, fieldState }) => (
            <Field data-invalid={fieldState.invalid}>
              <FieldLabel htmlFor={field.name}>Message</FieldLabel>
              <Input
                {...field}
                id={field.name}
                aria-invalid={fieldState.invalid}
                placeholder="Votre message..."
              />
              {fieldState.invalid && (
                <FieldError errors={[fieldState.error]} />
              )}
            </Field>
          )}
        />

        <Button type="submit" className="w-full">
          Envoyer
        </Button>

      </form>
    </div>
  )
}

Flux de validation du formulaire

graph LR
    A[Utilisateur soumet] --> B{Validation Zod}
    B -->|Succès| C[onSubmit appelé]
    B -->|Échec| D[Messages d'erreur affichés]
    D --> E[Utilisateur corrige]
    E --> A

Éléments clés du formulaire

Composante Rôle
Controller Connecte un champ au formulaire via control et name
Field Regroupe le label, le champ et le message d'erreur ; reçoit data-invalid pour le style
FieldLabel Étiquette du champ, liée au champ via htmlFor
FieldError Affiche automatiquement le message d'erreur Zod
fieldState.invalid Booléen indiquant si le champ est en erreur
fieldState.error Objet contenant le message d'erreur

Grille de produits

La composante Card est idéale pour afficher des éléments dans une grille. Combinée avec Badge et Button, elle permet de créer des fiches produits complètes.

Définir le type Produit

src/types/Produit.ts
export type Produit = {
  id: number
  nom: string
  prix: number
  categorie: string
  description: string
  image: string
}

Composante CarteProduit

src/components/CarteProduit/CarteProduit.tsx
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Produit } from "@/types/Produit"

type Props = {
  produit: Produit
}

export function CarteProduit({ produit }: Props) {
  return (
    <Card className="flex flex-col">
      <CardHeader className="p-0">
        <img
          src={produit.image}
          alt={produit.nom}
          className="w-full h-48 object-cover rounded-t-lg"
        />
        <div className="px-6 pt-4 flex items-start justify-between gap-2">
          <CardTitle className="text-lg">{produit.nom}</CardTitle>
          <Badge variant="secondary">{produit.categorie}</Badge>
        </div>
        <CardDescription className="px-6">
          {produit.description}
        </CardDescription>
      </CardHeader>

      <CardContent className="flex-1" />

      <CardFooter className="flex items-center justify-between">
        <span className="text-xl font-bold">{produit.prix.toFixed(2)} $</span>
        <Button>Ajouter au panier</Button>
      </CardFooter>
    </Card>
  )
}

Composante GrilleProduits

src/components/GrilleProduits/GrilleProduits.tsx
import { CarteProduit } from "@/components/CarteProduit/CarteProduit"
import { Produit } from "@/types/Produit"

const produits: Produit[] = [
  {
    id: 1,
    nom: "Écouteurs sans fil",
    prix: 79.99,
    categorie: "Électronique",
    description: "Son haute fidélité avec réduction de bruit active.",
    image: "https://placehold.co/400x300",
  },
  {
    id: 2,
    nom: "Sac à dos urbain",
    prix: 49.95,
    categorie: "Accessoires",
    description: "Design moderne avec compartiment pour ordinateur 15 pouces.",
    image: "https://placehold.co/400x300",
  },
  {
    id: 3,
    nom: "Montre intelligente",
    prix: 199.0,
    categorie: "Électronique",
    description: "Suivi d'activité, notifications et autonomie de 7 jours.",
    image: "https://placehold.co/400x300",
  },
  {
    id: 4,
    nom: "Chaussures de course",
    prix: 124.99,
    categorie: "Sport",
    description: "Légères et confortables pour la course sur route.",
    image: "https://placehold.co/400x300",
  },
]

export function GrilleProduits() {
  return (
    <section className="p-6">
      <h2 className="text-3xl font-bold mb-6">Nos produits</h2>
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
        {produits.map((produit) => (
          <CarteProduit key={produit.id} produit={produit} />
        ))}
      </div>
    </section>
  )
}

Éléments clés de Card

Composante Rôle
Card Conteneur principal avec bordure et ombre
CardHeader Zone supérieure (titre, description)
CardTitle Titre de la carte
CardDescription Texte descriptif secondaire
CardContent Corps de la carte
CardFooter Zone inférieure (actions)

Intégration dans App.tsx

Voici comment assembler les trois composantes dans l'application principale :

src/App.tsx
import { NavBar } from "@/components/NavBar/NavBar"
import { GrilleProduits } from "@/components/GrilleProduits/GrilleProduits"

function App() {
  return (
    <div className="min-h-screen bg-background">
      <NavBar />
      <main>
        <GrilleProduits />
      </main>
    </div>
  )
}

export default App

Thème foncé avec shadcn

Le thème foncé est simplement une classe CSS avec toutes les variables de couleurs. Vous pouvez activer le thème en faisant ceci :

src/App.tsx
  const root = window.document.documentElement;

  root.classList.add('dark');