Guide Complet shadcn/ui avec Next.js : Construire une Interface Moderne

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Introduction

shadcn/ui est devenu la référence incontournable pour construire des interfaces modernes avec React et Next.js. Contrairement aux bibliothèques de composants traditionnelles comme Material UI ou Chakra UI, shadcn/ui adopte une approche radicalement différente : au lieu d'installer un package npm, vous copiez les composants directement dans votre projet. Cela vous donne un contrôle total sur le code, le style et le comportement de chaque composant.

Dans ce tutoriel, vous allez apprendre à mettre en place shadcn/ui dans un projet Next.js, personnaliser le thème, construire des composants complexes comme des formulaires et des tableaux de données, et appliquer les bonnes pratiques pour créer une application de qualité professionnelle.

Ce que vous allez apprendre

  • Installer et configurer shadcn/ui dans un projet Next.js
  • Utiliser les composants les plus courants (Button, Card, Dialog, Form)
  • Personnaliser le thème avec des variables CSS
  • Construire un formulaire complet avec validation via React Hook Form et Zod
  • Créer un tableau de données interactif avec tri et filtrage
  • Implémenter le mode sombre (dark mode)
  • Organiser vos composants de manière maintenable

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 18+ installé sur votre machine
  • Des connaissances de base en React et TypeScript
  • Une familiarité avec Tailwind CSS
  • Un éditeur de code (VS Code recommandé)

Étape 1 : Créer le projet Next.js

Commencez par créer un nouveau projet Next.js avec TypeScript et Tailwind CSS :

npx create-next-app@latest mon-app-shadcn --typescript --tailwind --eslint --app --src-dir
cd mon-app-shadcn

Sélectionnez les options suivantes lors de la configuration :

  • Would you like to use App Router? Yes
  • Would you like to customize the default import alias? Yes, utilisez @/*

Étape 2 : Installer shadcn/ui

Exécutez la commande d'initialisation de shadcn/ui :

npx shadcn@latest init

L'assistant de configuration vous posera plusieurs questions :

Which style would you like to use? › New York
Which color would you like to use as base color? › Neutral
Do you want to use CSS variables for colors? › yes

Cette commande va :

  • Créer un fichier components.json à la racine du projet
  • Ajouter les utilitaires nécessaires dans lib/utils.ts
  • Configurer les variables CSS dans votre fichier globals.css
  • Mettre à jour tailwind.config.ts avec les chemins des composants

Vérifions le fichier components.json généré :

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "src/app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}

Étape 3 : Ajouter vos premiers composants

shadcn/ui propose une CLI pour ajouter des composants individuellement. Ajoutons les composants essentiels :

npx shadcn@latest add button card input label

Chaque composant est copié dans src/components/ui/. Examinons le composant Button :

// src/components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
 
const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)
 
export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}
 
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"
 
export { Button, buttonVariants }

Le pattern est clair : chaque composant utilise cva (Class Variance Authority) pour gérer les variantes et cn (un wrapper autour de clsx et tailwind-merge) pour fusionner les classes CSS.

Étape 4 : Créer une page avec les composants

Modifions la page d'accueil pour utiliser nos composants :

// src/app/page.tsx
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
 
export default function Home() {
  return (
    <main className="container mx-auto py-10">
      <h1 className="text-4xl font-bold mb-8">
        Mon Application shadcn/ui
      </h1>
 
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        <Card>
          <CardHeader>
            <CardTitle>Composants Accessibles</CardTitle>
            <CardDescription>
              Basés sur Radix UI, tous les composants sont
              accessibles par défaut.
            </CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-sm text-muted-foreground">
              Navigation au clavier, lecteurs d'écran, et
              conformité ARIA intégrés.
            </p>
          </CardContent>
          <CardFooter>
            <Button>En savoir plus</Button>
          </CardFooter>
        </Card>
 
        <Card>
          <CardHeader>
            <CardTitle>Personnalisable</CardTitle>
            <CardDescription>
              Le code est dans votre projet. Modifiez tout
              selon vos besoins.
            </CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-sm text-muted-foreground">
              Variables CSS, Tailwind, et composants
              modifiables à volonté.
            </p>
          </CardContent>
          <CardFooter>
            <Button variant="outline">Explorer</Button>
          </CardFooter>
        </Card>
 
        <Card>
          <CardHeader>
            <CardTitle>TypeScript Natif</CardTitle>
            <CardDescription>
              Types stricts et autocomplétion pour une
              expérience développeur optimale.
            </CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-sm text-muted-foreground">
              Chaque composant est entièrement typé avec
              des props bien définies.
            </p>
          </CardContent>
          <CardFooter>
            <Button variant="secondary">Découvrir</Button>
          </CardFooter>
        </Card>
      </div>
    </main>
  )
}

Lancez le serveur de développement pour voir le résultat :

npm run dev

Étape 5 : Personnaliser le thème

Le système de theming de shadcn/ui repose sur des variables CSS. Ouvrez src/app/globals.css pour voir les variables générées :

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 0 0% 3.9%;
    --card: 0 0% 100%;
    --card-foreground: 0 0% 3.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 0 0% 3.9%;
    --primary: 0 0% 9%;
    --primary-foreground: 0 0% 98%;
    --secondary: 0 0% 96.1%;
    --secondary-foreground: 0 0% 9%;
    --muted: 0 0% 96.1%;
    --muted-foreground: 0 0% 45.1%;
    --accent: 0 0% 96.1%;
    --accent-foreground: 0 0% 9%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 0 0% 98%;
    --border: 0 0% 89.8%;
    --input: 0 0% 89.8%;
    --ring: 0 0% 3.9%;
    --radius: 0.5rem;
  }
 
  .dark {
    --background: 0 0% 3.9%;
    --foreground: 0 0% 98%;
    --primary: 0 0% 98%;
    --primary-foreground: 0 0% 9%;
    /* ... autres variables sombres */
  }
}

Pour créer un thème personnalisé, modifiez ces variables. Par exemple, pour un thème bleu professionnel :

:root {
  --primary: 221.2 83.2% 53.3%;
  --primary-foreground: 210 40% 98%;
  --secondary: 210 40% 96.1%;
  --secondary-foreground: 222.2 47.4% 11.2%;
  --accent: 210 40% 96.1%;
  --accent-foreground: 222.2 47.4% 11.2%;
  --radius: 0.75rem;
}

Vous pouvez aussi utiliser le générateur de thèmes en ligne de shadcn/ui pour créer visuellement votre palette de couleurs et copier les variables CSS générées.

Étape 6 : Formulaire avec validation

L'un des cas d'usage les plus courants est la création de formulaires. shadcn/ui s'intègre parfaitement avec React Hook Form et Zod pour la validation.

Installez les dépendances nécessaires :

npx shadcn@latest add form select textarea toast
npm install zod

Créez un formulaire de contact complet :

// src/components/contact-form.tsx
"use client"
 
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { useToast } from "@/hooks/use-toast"
 
const formSchema = z.object({
  nom: z
    .string()
    .min(2, "Le nom doit contenir au moins 2 caractères"),
  email: z
    .string()
    .email("Adresse email invalide"),
  sujet: z
    .string()
    .min(1, "Veuillez sélectionner un sujet"),
  message: z
    .string()
    .min(10, "Le message doit contenir au moins 10 caractères")
    .max(500, "Le message ne doit pas dépasser 500 caractères"),
})
 
type FormValues = z.infer<typeof formSchema>
 
export function ContactForm() {
  const { toast } = useToast()
 
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      nom: "",
      email: "",
      sujet: "",
      message: "",
    },
  })
 
  function onSubmit(values: FormValues) {
    console.log(values)
    toast({
      title: "Message envoyé !",
      description: "Nous vous répondrons dans les 24 heures.",
    })
    form.reset()
  }
 
  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="space-y-6"
      >
        <FormField
          control={form.control}
          name="nom"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Nom complet</FormLabel>
              <FormControl>
                <Input placeholder="Jean Dupont" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
 
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input
                  type="email"
                  placeholder="jean@exemple.fr"
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
 
        <FormField
          control={form.control}
          name="sujet"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Sujet</FormLabel>
              <Select
                onValueChange={field.onChange}
                defaultValue={field.value}
              >
                <FormControl>
                  <SelectTrigger>
                    <SelectValue placeholder="Choisir un sujet" />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  <SelectItem value="general">
                    Question générale
                  </SelectItem>
                  <SelectItem value="support">
                    Support technique
                  </SelectItem>
                  <SelectItem value="commercial">
                    Demande commerciale
                  </SelectItem>
                  <SelectItem value="partenariat">
                    Partenariat
                  </SelectItem>
                </SelectContent>
              </Select>
              <FormMessage />
            </FormItem>
          )}
        />
 
        <FormField
          control={form.control}
          name="message"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Message</FormLabel>
              <FormControl>
                <Textarea
                  placeholder="Décrivez votre demande..."
                  className="min-h-[120px]"
                  {...field}
                />
              </FormControl>
              <FormDescription>
                Entre 10 et 500 caractères.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
 
        <Button type="submit" className="w-full">
          Envoyer le message
        </Button>
      </form>
    </Form>
  )
}

Ce formulaire offre :

  • Validation côté client avec des messages d'erreur en français
  • Types TypeScript automatiquement inférés depuis le schéma Zod
  • Accessibilité : labels associés, messages d'erreur liés aux champs
  • Feedback utilisateur via les notifications toast

Étape 7 : Tableau de données interactif

Les tableaux de données sont un autre composant fréquemment utilisé. shadcn/ui fournit un composant Table qui s'intègre avec TanStack Table pour le tri, le filtrage et la pagination.

npx shadcn@latest add table badge dropdown-menu
npm install @tanstack/react-table

Créez un tableau de données pour afficher une liste d'utilisateurs :

// src/components/users-table.tsx
"use client"
 
import { useState } from "react"
import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getSortedRowModel,
  SortingState,
  useReactTable,
} from "@tanstack/react-table"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ArrowUpDown } from "lucide-react"
 
type User = {
  id: string
  nom: string
  email: string
  role: "admin" | "editeur" | "lecteur"
  statut: "actif" | "inactif"
}
 
const data: User[] = [
  {
    id: "1",
    nom: "Alice Martin",
    email: "alice@exemple.fr",
    role: "admin",
    statut: "actif",
  },
  {
    id: "2",
    nom: "Bob Dupont",
    email: "bob@exemple.fr",
    role: "editeur",
    statut: "actif",
  },
  {
    id: "3",
    nom: "Claire Bernard",
    email: "claire@exemple.fr",
    role: "lecteur",
    statut: "inactif",
  },
]
 
const columns: ColumnDef<User>[] = [
  {
    accessorKey: "nom",
    header: ({ column }) => (
      <Button
        variant="ghost"
        onClick={() =>
          column.toggleSorting(column.getIsSorted() === "asc")
        }
      >
        Nom
        <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
  },
  {
    accessorKey: "email",
    header: "Email",
  },
  {
    accessorKey: "role",
    header: "Rôle",
    cell: ({ row }) => {
      const role = row.getValue("role") as string
      const variant =
        role === "admin"
          ? "default"
          : role === "editeur"
            ? "secondary"
            : "outline"
      return <Badge variant={variant}>{role}</Badge>
    },
  },
  {
    accessorKey: "statut",
    header: "Statut",
    cell: ({ row }) => {
      const statut = row.getValue("statut") as string
      return (
        <Badge
          variant={statut === "actif" ? "default" : "destructive"}
        >
          {statut}
        </Badge>
      )
    },
  },
]
 
export function UsersTable() {
  const [sorting, setSorting] = useState<SortingState>([])
  const [globalFilter, setGlobalFilter] = useState("")
 
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onSortingChange: setSorting,
    onGlobalFilterChange: setGlobalFilter,
    state: { sorting, globalFilter },
  })
 
  return (
    <div className="space-y-4">
      <Input
        placeholder="Rechercher un utilisateur..."
        value={globalFilter}
        onChange={(e) => setGlobalFilter(e.target.value)}
        className="max-w-sm"
      />
      <div className="rounded-md border">
        <Table>
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => (
                  <TableHead key={header.id}>
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                  </TableHead>
                ))}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {table.getRowModel().rows.length > 0 ? (
              table.getRowModel().rows.map((row) => (
                <TableRow key={row.id}>
                  {row.getVisibleCells().map((cell) => (
                    <TableCell key={cell.id}>
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext()
                      )}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            ) : (
              <TableRow>
                <TableCell
                  colSpan={columns.length}
                  className="h-24 text-center"
                >
                  Aucun résultat.
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </div>
    </div>
  )
}

Étape 8 : Implémenter le mode sombre

shadcn/ui supporte nativement le mode sombre grâce aux variables CSS. Installez next-themes pour gérer le basculement :

npm install next-themes
npx shadcn@latest add dropdown-menu

Créez un provider de thème :

// src/components/theme-provider.tsx
"use client"
 
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
 
export function ThemeProvider({
  children,
  ...props
}: React.ComponentProps<typeof NextThemesProvider>) {
  return (
    <NextThemesProvider {...props}>
      {children}
    </NextThemesProvider>
  )
}

Ajoutez-le au layout racine :

// src/app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="fr" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

Créez un bouton de basculement de thème :

// src/components/theme-toggle.tsx
"use client"
 
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
 
export function ThemeToggle() {
  const { setTheme } = useTheme()
 
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Changer le thème</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>
          Clair
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>
          Sombre
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>
          Système
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Étape 9 : Composants Dialog et Sheet

Les modales et panneaux latéraux sont essentiels dans les applications modernes :

npx shadcn@latest add dialog sheet

Exemple de dialogue de confirmation :

// src/components/confirm-dialog.tsx
"use client"
 
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
 
interface ConfirmDialogProps {
  titre: string
  description: string
  onConfirm: () => void
  children: React.ReactNode
}
 
export function ConfirmDialog({
  titre,
  description,
  onConfirm,
  children,
}: ConfirmDialogProps) {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        {children}
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>{titre}</AlertDialogTitle>
          <AlertDialogDescription>
            {description}
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Annuler</AlertDialogCancel>
          <AlertDialogAction onClick={onConfirm}>
            Confirmer
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Étape 10 : Organiser les composants

Pour maintenir un projet propre, adoptez cette structure :

src/
├── components/
│   ├── ui/           # Composants shadcn/ui (générés)
│   │   ├── button.tsx
│   │   ├── card.tsx
│   │   ├── dialog.tsx
│   │   └── ...
│   ├── forms/        # Formulaires composés
│   │   ├── contact-form.tsx
│   │   └── login-form.tsx
│   ├── tables/       # Tableaux de données
│   │   └── users-table.tsx
│   ├── layout/       # Navigation, Footer, etc.
│   │   ├── header.tsx
│   │   ├── sidebar.tsx
│   │   └── footer.tsx
│   └── shared/       # Composants réutilisables personnalisés
│       ├── confirm-dialog.tsx
│       └── theme-toggle.tsx
├── hooks/            # Hooks personnalisés
│   └── use-toast.ts
└── lib/
    └── utils.ts      # Utilitaire cn()

Règles d'organisation :

  • Ne modifiez jamais les fichiers dans ui/ pour des changements spécifiques à un cas d'usage. Créez plutôt un wrapper dans shared/
  • Préfixez les composants composés avec leur contexte : ContactForm, UsersTable
  • Exportez depuis des fichiers index si vous avez beaucoup de composants dans un dossier

Bonnes pratiques

1. Utiliser la fonction cn() systématiquement

// Toujours utiliser cn() pour combiner les classes
import { cn } from "@/lib/utils"
 
function MonComposant({ className }: { className?: string }) {
  return (
    <div className={cn("p-4 rounded-lg", className)}>
      Contenu
    </div>
  )
}

2. Composition plutôt que configuration

// Préférer la composition
<Card>
  <CardHeader>
    <CardTitle>Titre</CardTitle>
  </CardHeader>
  <CardContent>Contenu</CardContent>
</Card>
 
// Plutôt que des props complexes
// <Card title="Titre" content="Contenu" /> // À éviter

3. Accessibilité intégrée

shadcn/ui est construit sur Radix UI, qui gère automatiquement :

  • La navigation au clavier
  • Les attributs ARIA
  • Le piège de focus dans les modales
  • Les annonces pour les lecteurs d'écran

Assurez-vous de toujours inclure des labels pour les champs de formulaire et du texte alternatif pour les éléments visuels.

4. Performances avec React Server Components

shadcn/ui fonctionne avec les React Server Components. Les composants qui ne nécessitent pas d'interactivité peuvent rester comme composants serveur :

// Ce composant peut être un Server Component
// Pas besoin de "use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
 
export function StatCard({ titre, valeur }: { titre: string; valeur: string }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle className="text-sm font-medium">{titre}</CardTitle>
      </CardHeader>
      <CardContent>
        <p className="text-2xl font-bold">{valeur}</p>
      </CardContent>
    </Card>
  )
}

Dépannage

Le composant ne s'affiche pas correctement

Vérifiez que tailwind.config.ts inclut le chemin des composants :

content: [
  "./src/**/*.{ts,tsx}",
  "./components/**/*.{ts,tsx}",
],

Erreur "Module not found" après ajout d'un composant

Redémarrez le serveur de développement. Parfois les nouveaux fichiers ne sont pas détectés par le hot reload :

# Ctrl+C puis
npm run dev

Les styles du mode sombre ne fonctionnent pas

Assurez-vous que darkMode: "class" est dans votre tailwind.config.ts et que le ThemeProvider enveloppe votre application.

Prochaines étapes

Maintenant que vous maîtrisez les bases de shadcn/ui avec Next.js, voici comment aller plus loin :

  • Explorez plus de composants : shadcn/ui propose plus de 40 composants (Accordion, Calendar, Combobox, Command palette, etc.)
  • Créez un Design System : utilisez les variables CSS et les variantes pour créer une identité visuelle cohérente
  • Intégrez avec une API : connectez vos formulaires et tableaux à une API backend avec TanStack Query
  • Ajoutez des animations : utilisez Framer Motion pour des transitions fluides entre les états

Conclusion

shadcn/ui représente une évolution majeure dans la façon dont nous construisons des interfaces avec React. En vous donnant la propriété complète du code, il élimine les contraintes des bibliothèques de composants traditionnelles tout en offrant une base solide, accessible et personnalisable.

Les points clés à retenir :

  • Propriété du code : vous contrôlez chaque aspect de vos composants
  • Accessibilité native : Radix UI gère les interactions complexes
  • Theming flexible : les variables CSS permettent une personnalisation totale
  • Intégration TypeScript : types stricts pour une meilleure expérience développeur
  • Écosystème riche : React Hook Form, Zod, TanStack Table s'intègrent naturellement

Avec cette base, vous êtes prêt à construire des applications professionnelles avec une interface utilisateur de haute qualité.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Créer un serveur MCP en TypeScript (2026) — Tutoriel pas à pas.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire une application full-stack en temps réel avec Convex et Next.js 15

Apprenez à construire une application full-stack en temps réel avec Convex et Next.js 15. Ce tutoriel couvre la conception de schémas, les requêtes, les mutations, les abonnements en temps réel, l'authentification et le téléchargement de fichiers — le tout avec une sécurité de types de bout en bout.

30 min read·