Construire un squelette admin avec shadcn/ui : Sidebar + Layout, bonnes pratiques

Pas à pas, construisez un squelette admin extensible — de la Sidebar à l’intégration Next.js Layout, avec du code prêt à l’emploi.
La semaine dernière, j’ai pris un projet de back-office admin — shadcn/ui m’est venu tout de suite en tête. J’avais déjà utilisé Ant Design et MUI ; personnaliser les styles restait pénible — soit des tonnes d’overrides, soit des contraintes imposées par le framework.
shadcn/ui, c’est autre chose. Modèle « Copy-paste » : le code est dans votre projet, vous modifiez comme vous voulez. Deux semaines d’usage plus tard, le constat est net. Surtout la Sidebar avec l’App Router Next.js : le squelette admin devient beaucoup plus clair.
Cet article rassemble ma pratique : partir de zéro pour un layout admin extensible.
1. Pourquoi shadcn/ui Sidebar ?
Commençons par les pièges que j’ai rencontrés.
Avec Ant Design Pro, le démarrage est rapide — mais à long terme ça se complique : modifier le style de la sidebar, c’est fouiller la doc ; une interaction personnalisée bute souvent sur les limites du framework. MUI pose le même type de problème : le theming est flexible, mais suppose Material Design.
Limites des approches classiques
Ant Design : fonctionnalités complètes, coût de personnalisation élevé. Arrondir les coins de la sidebar peut demander trois niveaux d’overrides CSS.
MUI : design system complet, courbe d’apprentissage raide. Styled Components — une semaine d’onboarding pour les nouveaux du équipe, en pratique.
Tout coder soi-même : contrôle total, mais une sidebar responsive avec accessibilité et navigation clavier ? Comptez au minimum trois jours.
La réponse de shadcn/ui
shadcn/ui emprunte une autre voie :
- Modèle Copy-paste : le code du composant est dans votre projet, sans boîte noire
- Fondation Radix UI : accessibilité intégrée — navigation clavier, attributs ARIA déjà gérés
- Piloté par Tailwind CSS : le style ce sont des classes, vous modifiez sans guerre d’overrides
J’ai vu des équipes migrer de MUI vers shadcn/ui pour une raison simple : elles veulent du contrôle, pas un template clé en main.
Cas d’usage
Si vous travaillez sur :
- Un back-office admin de taille moyenne
- La console d’un produit SaaS
- Un outil interne ou une plateforme ops
shadcn/ui Sidebar vaut le coup. Ce n’est pas un template admin complet, mais un squelette suffisamment flexible.
2. Architecture du composant Sidebar
Avant de coder, comprenons l’écosystème shadcn/ui Sidebar — la doc officielle est claire sur ce point, je résume.
Liste des composants clés
La Sidebar est un ensemble de composants aux rôles distincts :
SidebarProvider // Contexte d'état, enveloppe toute l'app
Sidebar // Conteneur de la barre latérale
SidebarHeader // Zone fixe en haut, logo
SidebarContent // Zone scrollable, menus
SidebarGroup // Groupe de menu
SidebarMenu // Liste de menu
SidebarMenuItem // Entrée de menu
SidebarMenuButton // Bouton de menu (support Link)
SidebarFooter // Zone fixe en bas, infos utilisateur
SidebarTrigger // Bouton replier/déplier
SidebarInset // Wrapper de la zone principale
Beaucoup de composants, mais la hiérarchie est simple :
SidebarProvider
├── Sidebar
│ ├── SidebarHeader
│ ├── SidebarContent
│ │ └── SidebarGroup
│ │ └── SidebarMenu
│ │ └── SidebarMenuItem
│ │ └── SidebarMenuButton
│ └── SidebarFooter
└── SidebarInset
└── {children}
Gestion de l’état
L’état replié est géré par SidebarProvider. Deux modes :
Mode non contrôlé (recommandé) :
<SidebarProvider defaultOpen={true}>
<Sidebar />
</SidebarProvider>
Mode contrôlé :
const [open, setOpen] = useState(true);
<SidebarProvider open={open} onOpenChange={setOpen}>
<Sidebar />
</SidebarProvider>
En général, le mode non contrôlé suffit. Le mode contrôlé sert si vous devez piloter la Sidebar ailleurs (par ex. un interrupteur dans les paramètres utilisateur).
Design responsive
La Sidebar gère le responsive en interne :
- Desktop : barre fixe à gauche, repli possible via
SidebarTrigger - Mobile : devient un tiroir Sheet, ouvert au clic sur le Trigger
La logique est dans le composant — configurez SidebarProvider, le reste suit.
3. Intégration Next.js Layout en pratique
Les concepts posés, place à l’intégration dans le système Layout de Next.js.
3.1 Structure du projet
Je recommande les Route Groups Next.js pour organiser les layouts : pages à usages différents, layouts différents, URL inchangées.
app/
├── layout.tsx # Root Layout (global)
├── (marketing)/ # Groupe marketing (Landing, About)
│ ├── layout.tsx # Sans Sidebar
│ └── page.tsx # Accueil
├── (dashboard)/ # Groupe back-office
│ ├── layout.tsx # Layout avec Sidebar
│ ├── page.tsx # Page Dashboard
│ ├── users/
│ │ └── page.tsx # Gestion utilisateurs
│ └── settings/
│ └── page.tsx # Paramètres système
└── (auth)/ # Groupe authentification
├── layout.tsx # Layout centré
├── login/
│ └── page.tsx # Connexion
└── register/
└── page.tsx # Inscription
Avantages :
- Isolation des layouts : marketing sans Sidebar, back-office avec — séparation naturelle via Route Groups
- URLs sobres :
(dashboard)n’apparaît pas dans l’URL,/usersreste/users - Extensible : nouveau groupe = nouveau dossier
3.2 Configuration du Root Layout
Le Root Layout est l’entrée de l’app — thème, polices, SidebarProvider s’y configurent.
// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { SidebarProvider } from "@/components/ui/sidebar";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Admin Dashboard",
description: "Built with shadcn/ui and Next.js",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr-FR">
<body className={inter.className}>
<SidebarProvider>
{children}
</SidebarProvider>
</body>
</html>
);
}
Note : SidebarProvider est dans le Root Layout, pas le Dashboard Layout. L’état de la Sidebar persiste entre pages (de /users à /settings, l’état replié ne se perd pas).
3.3 Implémentation du Dashboard Layout
Le Dashboard Layout est le cœur du back-office — la Sidebar s’y introduit.
// app/(dashboard)/layout.tsx
import { AppSidebar } from "@/components/app-sidebar";
import { SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="/dashboard">
Administration
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Vue d'ensemble</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<main className="flex-1 p-4 pt-6">{children}</main>
</SidebarInset>
</gt;
);
}
Ce layout comprend :
- AppSidebar : sidebar personnalisée (section suivante)
- SidebarInset : wrapper du contenu principal, gère la largeur au repli
- Header : barre supérieure avec SidebarTrigger et fil d’Ariane
- Main : zone de contenu principal
3.4 Implémentation du composant AppSidebar
Implémentons la sidebar. Approche pilotée par configuration : le menu vit dans un fichier de config, le composant rend selon cette config.
D’abord la configuration de navigation :
// lib/navigation.ts
import {
Home,
Users,
Settings,
FileText,
BarChart3,
Shield,
} from "lucide-react";
export interface NavItem {
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
badge?: string;
}
export const navConfig: NavItem[] = [
{
title: "Vue d'ensemble",
href: "/dashboard",
icon: Home,
},
{
title: "Utilisateurs",
href: "/users",
icon: Users,
badge: "12", // badge
},
{
title: "Analytique",
href: "/analytics",
icon: BarChart3,
},
{
title: "Contenu",
href: "/content",
icon: FileText,
},
{
title: "Paramètres",
href: "/settings",
icon: Settings,
},
{
title: "Permissions",
href: "/permissions",
icon: Shield,
},
];
Puis AppSidebar :
// components/app-sidebar.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { navConfig } from "@/lib/navigation";
import { Logo } from "@/components/logo";
import { UserNav } from "@/components/user-nav";
export function AppSidebar() {
const pathname = usePathname();
return (
<Sidebar>
<SidebarHeader className="border-b border-border">
<Logo />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{navConfig.map((item) => {
const isActive = pathname === item.href;
return (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton
asChild
isActive={isActive}
tooltip={item.title}
>
<Link href={item.href}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
{item.badge && (
<span className="ml-auto text-xs bg-primary text-primary-foreground rounded-full px-2 py-0.5">
{item.badge}
</span>
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="border-t border-border">
<UserNav />
</SidebarFooter>
</Sidebar>
);
}
Point clé : surlignage de route. usePathname() récupère le chemin courant, comparaison avec item.href — si match, isActive={true} sur SidebarMenuButton, le composant applique le style actif.
3.5 Menu multi-niveaux
Pour un menu à deux niveaux, enveloppez SidebarGroup avec Collapsible :
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";
// Dans SidebarMenu
<Collapsible defaultOpen>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<Settings className="h-4 w-4" />
<span>Paramètres système</span>
<ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem>
<SidebarMenuSubButton href="/settings/general">
<span>Général</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton href="/settings/security">
<span>Sécurité</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
4. Fonctionnalités avancées
Le layout de base est en place — quelques ajouts utiles.
4.1 Contrôle d’accès (RBAC)
Beaucoup de back-offices affichent des menus selon le rôle. Simple : champ roles dans la config, filtrage au rendu.
Adapter la configuration :
// lib/navigation.ts
export interface NavItem {
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
roles?: string[]; // rôles autorisés
}
export const navConfig: NavItem[] = [
{
title: "Vue d'ensemble",
href: "/dashboard",
icon: Home,
// pas de roles — visible pour tous
},
{
title: "Utilisateurs",
href: "/users",
icon: Users,
roles: ["admin", "manager"], // admin et manager uniquement
},
{
title: "Permissions",
href: "/permissions",
icon: Shield,
roles: ["admin"], // admin uniquement
},
];
Filtrer selon le rôle dans AppSidebar :
// components/app-sidebar.tsx
import { useAuth } from "@/hooks/use-auth"; // supposons un hook auth
export function AppSidebar() {
const pathname = usePathname();
const { user } = useAuth(); // utilisateur courant
const filteredNav = navConfig.filter((item) => {
if (!item.roles) return true; // pas de restriction
return item.roles.some((role) => user?.roles?.includes(role));
});
return (
<Sidebar>
{/* ... */}
<SidebarMenu>
{filteredNav.map((item) => {
// ...
})}
</SidebarMenu>
{/* ... */}
</Sidebar>
);
}
Résultat : un utilisateur standard ne voit plus l’entrée « Permissions ».
4.2 Liens externes et séparateurs
Parfois la sidebar accueille des liens externes (doc, centre d’aide) ou des séparateurs entre groupes. shadcn/ui Sidebar le supporte :
<SidebarGroup>
<SidebarGroupLabel>Fonctions principales</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{/* entrées principales */}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Aide et support</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<a href="https://docs.example.com" target="_blank" rel="noopener">
<BookOpen className="h-4 w-4" />
<span>Documentation</span>
<ExternalLink className="ml-auto h-3 w-3" />
</a>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<a href="mailto:[email protected]">
<HelpCircle className="h-4 w-4" />
<span>Nous contacter</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
4.3 Recherche et raccourcis
Beaucoup de back-offices placent une recherche dans la sidebar, ou une recherche globale (Cmd+K). Le composant Command de shadcn/ui convient :
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command";
<SidebarGroup>
<SidebarGroupContent>
<Command className="rounded-lg border shadow-md">
<CommandInput placeholder="Rechercher dans le menu..." />
<CommandList>
<CommandEmpty>Aucun résultat</CommandEmpty>
<CommandGroup heading="Suggestions">
{navConfig.map((item) => (
<CommandItem key={item.href} onSelect={() => router.push(item.href)}>
<item.icon className="mr-2 h-4 w-4" />
{item.title}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</SidebarGroupContent>
</SidebarGroup>
5. Performance et bonnes pratiques
Quelques optimisations en conditions réelles.
Server Components en priorité
Next.js App Router : par défaut tout est Server Component. Les parties statiques de la Sidebar (logo, entrées fixes) peuvent rester en Server Component ; seules les parties interactives (surlignage, repli) passent en "use client".
Mon approche :
AppSidebaren"use client"(usePathname)- Parties statiques de
SidebarHeader,SidebarFooterextraites en Server Component - Config de navigation générée côté serveur, passée au composant client
Moins de JS côté client.
Chargement paresseux des grands menus
Des dizaines d’entrées ? Lazy loading avec React.lazy ou dynamic de Next.js :
import dynamic from "next/dynamic";
const AdminMenu = dynamic(() => import("./admin-menu"), {
loading: () => <SidebarMenuSkeleton />,
});
En pratique, la plupart des back-offices ont peu d’entrées — cas d’usage rare.
Points d’accessibilité
La Sidebar shadcn/ui repose sur Radix UI — l’accessibilité est largement intégrée. Quelques rappels :
- Icône + texte : pas d’icône seule — invisible pour les lecteurs d’écran
- Focus visible : ne masquez pas le style focus par défaut
- Navigation clavier : Tab et flèches doivent fonctionner
Radix gère l’essentiel ; si vous personnalisez, testez la navigation clavier.
6. Questions fréquentes
Q1 : l’état de la Sidebar se perd au refresh ?
Si SidebarProvider est dans le Dashboard Layout au lieu du Root Layout, l’état se réinitialise à chaque navigation. Remontez le Provider dans le Root Layout.
Q2 : fermer automatiquement la Sidebar sur mobile ?
Sur mobile, la Sidebar devient un Sheet. Fermez manuellement après clic sur une entrée :
const { setOpenMobile } = useSidebar();
<SidebarMenuButton
onClick={() => setOpenMobile(false)}
>
Q3 : personnaliser la largeur de la Sidebar ?
Variables CSS :
<Sidebar
style={{
"--sidebar-width": "280px",
"--sidebar-width-mobile": "100%",
}}
>
Ou modifiez la constante SIDEBAR_WIDTH dans sidebar.tsx.
Résumé
shadcn/ui Sidebar avec Next.js Layout — un squelette admin efficace. Points clés :
- Écosystème de composants : rôles de SidebarProvider, Sidebar, SidebarContent, etc.
- Intégration Layout : Route Groups pour isoler les layouts,
SidebarProviderdans le Root Layout - Piloté par config : menu dans un fichier de config, rendu par le composant — maintenance simple
- Surlignage de route :
usePathname()+ propisActive, direct - Contrôle d’accès : champ
rolesdans la config, filtrage au rendu
J’utilise cette architecture sur plusieurs projets — bonne extensibilité. Nouvelle page = une entrée dans navConfig, le composant fait le reste.
Des questions ? Commentaires bienvenus. Prochain article : shadcn/ui DataTable en pratique — à suivre si ça vous intéresse.
Références
Construire un squelette admin shadcn/ui Sidebar + Next.js Layout
Partir de zéro pour un layout admin extensible avec sidebar, surbrillance de route et contrôle d'accès
⏱️ Estimated time: 45 min
- 1
Step 1: Installer shadcn/ui et ajouter le composant Sidebar
Initialiser le projet et ajouter le composant via la CLI :
```bash
npx shadcn@latest init
npx shadcn@latest add sidebar
```
L'installation demande la configuration des styles — la valeur par défaut suffit. Le fichier sidebar.tsx apparaît ensuite dans components/ui. - 2
Step 2: Configurer le Root Layout
Envelopper avec SidebarProvider dans app/layout.tsx :
• Importer SidebarProvider
• Envelopper {children} dans la balise body
• Définir lang="fr-FR"
Ainsi l'état de la Sidebar reste persistant globalement. - 3
Step 3: Créer le Dashboard Layout
Créer app/(dashboard)/layout.tsx pour le layout back-office :
• Utiliser la syntaxe Route Groups (dashboard)
• Importer AppSidebar et SidebarInset
• Ajouter un Header et un fil d'Ariane en haut
Les Route Groups n'apparaissent pas dans l'URL — /dashboard mappe directement sur la racine. - 4
Step 4: Définir la configuration de navigation
Créer lib/navigation.ts :
• Définir l'interface NavItem (title, href, icon, badge)
• Exporter le tableau navConfig
• Optionnel : champ roles pour le contrôle d'accès
Piloté par config : un seul endroit à modifier pour un nouveau menu. - 5
Step 5: Implémenter le composant AppSidebar
Créer components/app-sidebar.tsx :
• Marquer avec "use client" comme composant client
• Obtenir la route courante via usePathname
• Parcourir navConfig pour rendre les entrées
• Définir isActive pour surligner la route correspondante - 6
Step 6: Ajouter le contrôle d'accès (optionnel)
Implémenter le filtrage RBAC :
• Ajouter le champ roles à l'interface NavItem
• Récupérer le rôle utilisateur via useAuth dans AppSidebar
• Filtrer les entrées avec filter
Sans champ roles, l'entrée est visible pour tous.
FAQ
Quelle différence entre shadcn/ui Sidebar et la sidebar Ant Design ?
SidebarProvider dans le Root Layout ou le Dashboard Layout ?
Comment fermer automatiquement la Sidebar sur mobile ?
```tsx
const { setOpenMobile } = useSidebar();
<SidebarMenuButton onClick={() => setOpenMobile(false)}>
```
Le tiroir se referme après le clic.
Comment personnaliser la largeur de la Sidebar ?
1. Variables CSS (recommandé) :
```tsx
<Sidebar style={{ "--sidebar-width": "280px" }} />
```
2. Modifier la constante SIDEBAR_WIDTH dans sidebar.tsx
Les variables CSS permettent des largeurs différentes par Sidebar.
Comment implémenter un menu multi-niveaux ?
• CollapsibleTrigger pour le bouton de premier niveau
• CollapsibleContent avec SidebarMenuSub et sous-entrées
• Icône ChevronDown pour l'état déplié
Code complet en section 3.5.
Quelle accessibilité pour shadcn/ui Sidebar ?
13 min de lecture · Publié le: 27 mars 2026 · Mis à jour le: 27 juil. 2026
Tailwind & shadcn/ui en pratique
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
shadcn/ui : guide complet d'installation et de personnalisation du thème (variables CSS)
Installation et configuration de shadcn/ui, personnalisation du thème avec variables CSS, couleurs OKLCH et mode sombre. Bonnes pratiques de design de marque — prise en main en 5 minutes.
Partie 4 sur 14
Suivant
Mise en page responsive avec Tailwind : container queries et stratégie de breakpoints
Plongez dans les container queries et la stratégie de breakpoints de Tailwind CSS : du viewport au conteneur, pour une mise en page responsive au niveau composant.
Partie 6 sur 14



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire