Composantes UI : shadcn/ui¶
Le cours de développement Web 3 n'a pas un focus sur l'utilisation de CSS comme le cours de design Web. Pour aider à faire des pages Web avec un look intéressant, je vais vous présenter une collections de composantes, qui utilise différentes technologies, dont TailWindCSS pour avoir un look moderne et épuré.
Prérequis¶
shadcn/ui nécessite Tailwind CSS. Bien préparer votre application 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 :
{
"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 :
2. Modifier vite.config.ts¶
Ajoutez la résolution de l'alias @/ dans la configuration Vite :
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 :
3. Initialiser shadcn/ui¶
L'assistant interactif vous posera quelques questions. Voici les choix recommandés :
? 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
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/ :
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
Barre de navigation¶
La composante NavigationMenu permet de créer une barre de navigation avec des menus déroulants accessibles.
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>
)
}
Manuel
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¶
Ajouter les composantes shadcn/ui nécessaires :
Exemple de formulaire de contact¶
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>
)
}
Manuel
Manuel
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¶
export type Produit = {
id: number
nom: string
prix: number
categorie: string
description: string
image: string
}
Composante CarteProduit¶
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¶
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>
)
}
Manuel
Intégration dans App.tsx¶
Voici comment assembler les trois composantes dans l'application principale :
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 :