Admin-Skeleton mit shadcn/ui: Sidebar + Layout – Best Practices
Schritt für Schritt ein skalierbares Admin-Skeleton – von der Sidebar-Komponente bis zur Next.js-Layout-Integration, mit Code zum direkten Einstieg.
Letzte Woche kam ein Admin-Dashboard-Projekt rein – als Erstes dachte ich an shadcn/ui. Vorher hatte ich Ant Design und MUI genutzt; Styles anzupassen war oft mühsam – entweder massenhaft Overrides oder festgelegte Design-Vorgaben.
shadcn/ui ist anders. Copy-paste: Code liegt in Ihrem Projekt, Änderungen sind jederzeit möglich. Nach zwei Wochen Einsatz überzeugt es – besonders die Sidebar mit dem App Router macht das Admin-Skeleton deutlich übersichtlicher.
Dieser Artikel fasst meine Praxis zusammen: von null ein erweiterbares Admin-Layout aufbauen.
1. Warum shadcn/ui Sidebar?
Zuerst ein paar Erfahrungen aus der Praxis.
Mit Ant Design Pro startet man schnell, aber langfristig wird es eng: Sidebar-Styles erfordern lange Docs-Recherche; individuelle Interaktionen stoßen an Framework-Grenzen. Bei MUI ähnlich – Themes sind flexibel, setzen aber Material Design voraus.
Schwächen klassischer Ansätze
Ant Design: Funktionsreich, hohe Anpassungskosten. Abgerundete Sidebar-Ecken können drei Ebenen CSS-Overrides bedeuten.
MUI: Vollständiges Designsystem, steile Lernkurve. Styled Components – für Team-Neulinge oft eine Woche Einarbeitung.
Selbst bauen: Volle Kontrolle, aber eine responsive Sidebar mit Barrierefreiheit und Tastaturnavigation? Realistisch mehrere Tage.
Der shadcn/ui-Ansatz
shadcn/ui geht einen anderen Weg:
- Copy-paste-Modell: Komponentencode im Projekt, keine Black-Box-Abhängigkeit
- Radix UI als Basis: Barrierefreiheit eingebaut – Tastatur, ARIA
- Tailwind CSS: Styles sind Klassen – anpassen ohne Override-Kampf
Viele Teams wechseln von MUI zu shadcn/ui, weil sie Kontrolle wollen, nicht ein fertiges Template.
Wann es passt
Wenn Sie bauen:
- mittelgroße Admin-Dashboards
- SaaS-Konsolen
- interne Tools oder Ops-Plattformen
lohnt sich shadcn/ui Sidebar. Kein komplettes Admin-Template – aber ein flexibles Gerüst.
2. Sidebar-Komponentenarchitektur
Vor dem Codieren: das Komponentensystem von shadcn/ui Sidebar. Die offizielle Doku ist hier klar – kurz zusammengefasst.
Kernkomponenten
Die Sidebar besteht aus mehreren Teilen:
SidebarProvider // State-Kontext, umschließt die App
Sidebar // Sidebar-Container
SidebarHeader // Fixer Kopfbereich, z. B. Logo
SidebarContent // Scrollbarer Bereich für Menüs
SidebarGroup // Menügruppe
SidebarMenu // Menüliste
SidebarMenuItem // Menüeintrag
SidebarMenuButton // Menübutton (unterstützt Link)
SidebarFooter // Fixer Fußbereich, z. B. User-Info
SidebarTrigger // Ein-/Ausklappen
SidebarInset // Wrapper für Hauptinhalt
Die Hierarchie:
SidebarProvider
├── Sidebar
│ ├── SidebarHeader
│ ├── SidebarContent
│ │ └── SidebarGroup
│ │ └── SidebarMenu
│ │ └── SidebarMenuItem
│ │ └── SidebarMenuButton
│ └── SidebarFooter
└── SidebarInset
└── {children}
State-Management
Der Einklapp-Status liegt beim SidebarProvider. Zwei Modi:
Unkontrolliert (empfohlen):
<SidebarProvider defaultOpen={true}>
<Sidebar />
</SidebarProvider>
Kontrolliert:
const [open, setOpen] = useState(true);
<SidebarProvider open={open} onOpenChange={setOpen}>
<Sidebar />
</SidebarProvider>
Meist reicht unkontrolliert. Kontrolliert nur, wenn Sie den State woanders steuern (z. B. in den Einstellungen).
Responsives Verhalten
Eingebaute Responsive-Logik:
- Desktop: Sidebar links fixiert, per
SidebarTriggereinklappbar - Mobil: automatisch Sheet-Drawer, Trigger öffnet Overlay
Intern in der Komponente – SidebarProvider konfigurieren, Rest erledigt die Sidebar.
3. Next.js Layout – Praxis
Konzept geklärt – jetzt Sidebar ins Next.js-Layout-System einbinden.
3.1 Projektstruktur
Empfehlung: Route Groups für unterschiedliche Layouts ohne URL-Änderung.
app/
├── layout.tsx # Root Layout (global)
├── (marketing)/ # Marketing (Landing, About)
│ ├── layout.tsx # ohne Sidebar
│ └── page.tsx # Startseite
├── (dashboard)/ # Dashboard-Gruppe
│ ├── layout.tsx # Layout mit Sidebar
│ ├── page.tsx # Dashboard-Start
│ ├── users/
│ │ └── page.tsx # Benutzerverwaltung
│ └── settings/
│ └── page.tsx # Einstellungen
└── (auth)/ # Auth-Gruppe
├── layout.tsx # zentriertes Layout
├── login/
│ └── page.tsx # Login
└── register/
└── page.tsx # Registrierung
Vorteile:
- Layout-Trennung: Marketing ohne Sidebar, Dashboard mit – natürlich getrennt
- Schlanke URLs:
(dashboard)erscheint nicht –/usersbleibt/users - Erweiterbar: neue Gruppe = neuer Ordner
3.2 Root Layout
Einstiegspunkt der App – global: Theme, Schrift, SidebarProvider.
// 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="de">
<body className={inter.className}>
<SidebarProvider>
{children}
</SidebarProvider>
</body>
</html>
);
}
SidebarProvider im Root Layout, nicht im Dashboard Layout – so bleibt der Einklapp-Status beim Wechsel von /users zu /settings erhalten.
3.3 Dashboard Layout
Kernlayout des Backends – hier kommt die Sidebar rein.
// 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">
Admin
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Übersicht</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<main className="flex-1 p-4 pt-6">{children}</main>
</SidebarInset>
</>
);
}
Enthalten:
- AppSidebar: eigene Sidebar (nächster Abschnitt)
- SidebarInset: Hauptbereich, passt Breite beim Einklappen an
- Header: Trigger + Breadcrumb
- Main: Seiteninhalt
3.4 AppSidebar implementieren
Konfigurationsgesteuert: Navigation in einer Config-Datei, Komponente rendert daraus.
Navigation definieren:
// 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: "Übersicht",
href: "/dashboard",
icon: Home,
},
{
title: "Benutzer",
href: "/users",
icon: Users,
badge: "12",
},
{
title: "Analytics",
href: "/analytics",
icon: BarChart3,
},
{
title: "Inhalte",
href: "/content",
icon: FileText,
},
{
title: "Einstellungen",
href: "/settings",
icon: Settings,
},
{
title: "Berechtigungen",
href: "/permissions",
icon: Shield,
},
];
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>
);
}
Route-Highlight: usePathname() liefert den aktuellen Pfad, Vergleich mit item.href, bei Match isActive={true} – die Komponente setzt den Aktiv-Stil.
3.5 Mehrstufiges Menü
Für Untermenüs Collapsible um SidebarGroup bzw. Menüeinträge:
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";
// in SidebarMenu
<Collapsible defaultOpen>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<Settings className="h-4 w-4" />
<span>Einstellungen</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>Allgemein</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton href="/settings/security">
<span>Sicherheit</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
4. Erweiterte Funktionen
Basis steht – ein paar praktische Erweiterungen.
4.1 Berechtigungen (RBAC)
Menüs je Rolle: roles in der Config, beim Rendern filtern.
Config erweitern:
// lib/navigation.ts
export interface NavItem {
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
roles?: string[];
}
export const navConfig: NavItem[] = [
{
title: "Übersicht",
href: "/dashboard",
icon: Home,
// ohne roles – für alle sichtbar
},
{
title: "Benutzer",
href: "/users",
icon: Users,
roles: ["admin", "manager"],
},
{
title: "Berechtigungen",
href: "/permissions",
icon: Shield,
roles: ["admin"],
},
];
In AppSidebar filtern:
// components/app-sidebar.tsx
import { useAuth } from "@/hooks/use-auth";
export function AppSidebar() {
const pathname = usePathname();
const { user } = useAuth();
const filteredNav = navConfig.filter((item) => {
if (!item.roles) return true;
return item.roles.some((role) => user?.roles?.includes(role));
});
return (
<Sidebar>
{/* ... */}
<SidebarMenu>
{filteredNav.map((item) => {
// ...
})}
</SidebarMenu>
{/* ... */}
</Sidebar>
);
}
Normale Nutzer sehen „Berechtigungen“ nicht mehr.
4.2 Externe Links und Trennlinien
Externe Links (Docs, Support) oder Gruppen mit Labels:
<SidebarGroup>
<SidebarGroupLabel>Hauptfunktionen</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{/* Hauptmenü */}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Hilfe & Support</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<a href="https://docs.example.com" target="_blank" rel="noopener">
<BookOpen className="h-4 w-4" />
<span>Dokumentation</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>Kontakt</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
4.3 Suche und Shortcuts
Suchfeld oder globale Suche (Cmd+K) – shadcn/ui Command:
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command";
<SidebarGroup>
<SidebarGroupContent>
<Command className="rounded-lg border shadow-md">
<CommandInput placeholder="Menü durchsuchen…" />
<CommandList>
<CommandEmpty>Keine Treffer</CommandEmpty>
<CommandGroup heading="Vorschläge">
{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 und Best Practices
Ein paar Punkte aus der Praxis.
Server Components bevorzugen
Im App Router sind Komponenten standardmäßig Server Components. Statische Teile der Sidebar (Logo, feste Menüpunkte) können Server Components bleiben; Interaktion (Highlight, Einklappen) braucht "use client".
Vorgehen:
AppSidebarals"use client"(wegenusePathname)- statische Teile in Header/Footer als eigene Server Components
- Nav-Config serverseitig erzeugen und an Client übergeben
Das reduziert Client-JavaScript.
Große Menüs lazy laden
Bei sehr vielen Einträgen: React.lazy oder Next.js dynamic:
import dynamic from "next/dynamic";
const AdminMenu = dynamic(() => import("./admin-menu"), {
loading: () => <SidebarMenuSkeleton />,
});
In den meisten Admins reicht das selten – Menüs sind meist überschaubar.
Barrierefreiheit
Radix UI deckt vieles ab. Trotzdem beachten:
- Icon + Text: nicht nur Icons – Screenreader brauchen Beschriftung
- Focus sichtbar: Standard-Focus-Stile nicht überschreiben
- Tastatur: Tab und Pfeiltasten testen
Bei eigenen Anpassungen Tastaturnavigation prüfen.
6. Häufige Fragen
F1: Sidebar-State nach Reload weg?
Wenn SidebarProvider nur im Dashboard Layout sitzt, setzt sich der State bei Navigation zurück. Provider ins Root Layout heben.
F2: Sidebar auf Mobil automatisch schließen?
Mobil wird die Sidebar zum Sheet. Nach Menüklick manuell schließen:
const { setOpenMobile } = useSidebar();
<SidebarMenuButton
onClick={() => setOpenMobile(false)}
>
F3: Sidebar-Breite anpassen?
CSS-Variablen:
<Sidebar
style={{
"--sidebar-width": "280px",
"--sidebar-width-mobile": "100%",
}}
>
Oder SIDEBAR_WIDTH in sidebar.tsx ändern.
Fazit
shadcn/ui Sidebar mit Next.js Layout ist ein effizientes Gerüst für Admin-Oberflächen. Kurz zusammengefasst:
- Komponentensystem: SidebarProvider, Sidebar, SidebarContent – Rollen verstehen
- Layout: Route Groups trennen Layouts,
SidebarProviderim Root - Config: Navigation zentral, Komponente rendert daraus
- Highlight:
usePathname()+isActive - RBAC:
rolesin der Config, beim Rendern filtern
Diese Struktur nutze ich in mehreren Projekten – neue Seite = ein Eintrag in navConfig.
Fragen gerne in den Kommentaren. Als Nächstes: shadcn/ui DataTable in der Praxis.
Referenzen
shadcn/ui Sidebar + Next.js Layout Admin-Skeleton aufbauen
Von null ein skalierbares Admin-Layout mit Sidebar, Route-Highlight und Berechtigungssteuerung erstellen
⏱️ Estimated time: 45 min
- 1
Step 1: shadcn/ui installieren und Sidebar-Komponente hinzufügen
Projekt per CLI initialisieren und Komponente hinzufügen:
```bash
npx shadcn@latest init
npx shadcn@latest add sidebar
```
Bei der Style-Konfiguration reicht die Standardeinstellung. Danach liegt sidebar.tsx unter components/ui. - 2
Step 2: Root Layout konfigurieren
In app/layout.tsx mit SidebarProvider wrappen:
• SidebarProvider importieren
• {children} im body-Tag einbetten
• lang="de" setzen
So bleibt der Sidebar-State global persistent. - 3
Step 3: Dashboard Layout erstellen
Unter app/(dashboard)/layout.tsx das Dashboard-Layout anlegen:
• Route-Groups-Syntax (dashboard) nutzen
• AppSidebar und SidebarInset importieren
• Header und Breadcrumb oben ergänzen
Route Groups erscheinen nicht in der URL – /dashboard mappt direkt auf den Root-Pfad. - 4
Step 4: Navigationskonfiguration definieren
Datei lib/navigation.ts anlegen:
• NavItem-Interface (title, href, icon, badge)
• navConfig-Array exportieren
• Optional roles-Feld für Berechtigungen
Konfigurationsgesteuert: neues Menü = eine Stelle ändern. - 5
Step 5: AppSidebar-Komponente implementieren
components/app-sidebar.tsx erstellen:
• Mit „use client“ als Client Component markieren
• Aktuelle Route per usePathname holen
• navConfig durchlaufen und Menüeinträge rendern
• Bei Route-Match isActive für Highlight setzen - 6
Step 6: Berechtigungssteuerung hinzufügen (optional)
RBAC-Filter implementieren:
• roles-Feld im NavItem-Interface
• Benutzerrolle per useAuth in AppSidebar holen
• Menüeinträge per filter einschränken
Einträge ohne roles-Feld sind für alle sichtbar.
FAQ
Was unterscheidet shadcn/ui Sidebar von Ant Design Sidebar?
SidebarProvider im Root Layout oder im Dashboard Layout?
Sidebar auf Mobilgeräten automatisch schließen?
```tsx
const { setOpenMobile } = useSidebar();
<SidebarMenuButton onClick={() => setOpenMobile(false)}>
```
Der Drawer schließt sich danach von selbst.
Sidebar-Breite anpassen?
1. CSS-Variablen (empfohlen):
```tsx
<Sidebar style={{ "--sidebar-width": "280px" }} />
```
2. Konstante SIDEBAR_WIDTH in sidebar.tsx ändern
CSS-Variablen erlauben unterschiedliche Breiten pro Sidebar.
Mehrstufiges Menü umsetzen?
• CollapsibleTrigger für den Button der ersten Ebene
• CollapsibleContent mit SidebarMenuSub und Unterpunkten
• ChevronDown-Icon zeigt den Aufklapp-Zustand
Vollständiger Code in Abschnitt 3.5.
Wie barrierefrei ist shadcn/ui Sidebar?
11 Min. Lesezeit · Veröffentlicht am: 27. März 2026 · Aktualisiert am: 13. Juli 2026
Tailwind & shadcn/ui in der Praxis
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
shadcn/ui Installation und Theme-Anpassung: Der vollständige Leitfaden (inkl. CSS-Variablen)
Ausführliche Anleitung zur Installation und Theme-Anpassung von shadcn/ui: CSS-Variablen, OKLCH-Farben und Dark-Mode-Umschaltung. Best Practices für markenspezifisches UI-Design – in 5 Minuten startklar.
Teil 4 von 14
Nächster
Tailwind Responsive Layout in der Praxis: Container Queries und Breakpoint-Strategie
Container Queries und Breakpoint-Strategien in Tailwind CSS – vom Viewport zum Container: responsives Design auf Komponentenebene meistern.
Teil 6 von 14
Ähnliche Beiträge
Tailwind v4 + Vite: 5-Minuten-Setup-Vorlage und Verzeichnisstruktur

Tailwind v4 + Vite: 5-Minuten-Setup-Vorlage und Verzeichnisstruktur
Tailwind CSS v4 – Neue Features: Performance, Konfiguration und Migrationsleitfaden


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen