Esqueleto de panel con shadcn/ui: mejores prácticas de Sidebar + Layout

Guía paso a paso para montar un esqueleto de panel extensible: del componente Sidebar a la integración con Layout de Next.js, con código listo para usar.
La semana pasada empecé un panel de administración y lo primero que probé fue shadcn/ui. Antes usé Ant Design y MUI; personalizar estilos era pesado — muchas capas de override o quedar atado al design system del framework.
shadcn/ui va por otro camino: copy-paste, el código vive en tu proyecto y lo cambias cuando quieras. Tras un par de semanas, el Sidebar junto al App Router de Next.js deja un esqueleto de backend muy limpio.
Este artículo ordena ese proceso: de cero a un layout que escala.
1. ¿Por qué Sidebar de shadcn/ui?
Lo que no funcionaba antes
Con Ant Design Pro arrancabas rápido, pero al crecer el proyecto redondear la barra lateral era un lío. MUI exige dominar Material Design para sacarle partido al theming.
La propuesta de shadcn/ui
- Copy-paste: sin caja negra en node_modules
- Radix UI: accesibilidad y teclado resueltos
- Tailwind CSS: estilos = clases, sin guerras de especificidad
Muchos equipos migran desde MUI porque quieren control, no una plantilla rígida.
Cuándo encaja
- Paneles de administración medianos
- Consolas SaaS
- Herramientas internas u operaciones
No es una plantilla completa; es un esqueleto flexible.
2. Arquitectura del Sidebar
Componentes principales
SidebarProvider // Contexto de estado, envuelve la app
Sidebar // Contenedor lateral
SidebarHeader // Zona superior fija (logo)
SidebarContent // Área con scroll (menú)
SidebarGroup // Agrupación de menú
SidebarMenu // Lista de menú
SidebarMenuItem // Ítem
SidebarMenuButton // Botón (compatible con Link)
SidebarFooter // Zona inferior (usuario)
SidebarTrigger // Botón colapsar/expandir
SidebarInset // Wrapper del contenido principal
Relación:
SidebarProvider
├── Sidebar
│ ├── SidebarHeader
│ ├── SidebarContent
│ │ └── SidebarGroup
│ │ └── SidebarMenu
│ │ └── SidebarMenuItem
│ │ └── SidebarMenuButton
│ └── SidebarFooter
└── SidebarInset
└── {children}
Estado
No controlado (recomendado):
<SidebarProvider defaultOpen={true}>
<Sidebar />
</SidebarProvider>
Controlado:
const [open, setOpen] = useState(true);
<SidebarProvider open={open} onOpenChange={setOpen}>
<Sidebar />
</SidebarProvider>
Usa controlado solo si otro módulo (p. ej. ajustes) debe manejar el Sidebar.
Responsive
- Escritorio: barra fija a la izquierda, colapsable con SidebarTrigger
- Móvil: Sheet (cajón) al pulsar el trigger
La lógica va dentro del componente; tú solo configuras SidebarProvider.
3. Integración con Layout de Next.js
3.1 Estructura del proyecto
Route Groups para layouts distintos sin tocar la URL:
app/
├── layout.tsx # Root Layout
├── (marketing)/ # Landing, About
│ ├── layout.tsx # Sin Sidebar
│ └── page.tsx
├── (dashboard)/ # Panel
│ ├── layout.tsx # Con Sidebar
│ ├── page.tsx
│ ├── users/
│ │ └── page.tsx
│ └── settings/
│ └── page.tsx
└── (auth)/ # Login, registro
├── layout.tsx
├── login/
└── register/
Ventajas: layouts aislados, URLs limpias (/users no /dashboard/users), fácil ampliar.
3.2 Root Layout
// 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="es">
<body className={inter.className}>
<SidebarProvider>
{children}
</SidebarProvider>
</body>
</html>
);
}
SidebarProvider aquí — no en Dashboard Layout — para que el estado colapsado sobreviva entre /users y /settings.
3.3 Dashboard Layout
// 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">
Panel
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Resumen</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<main className="flex-1 p-4 pt-6">{children}</main>
</SidebarInset>
</>
);
}
Incluye AppSidebar, SidebarInset, header con trigger y migas, y main.
3.4 AppSidebar
Configuración centralizada:
// 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: "Resumen",
href: "/dashboard",
icon: Home,
},
{
title: "Usuarios",
href: "/users",
icon: Users,
badge: "12",
},
{
title: "Analítica",
href: "/analytics",
icon: BarChart3,
},
{
title: "Contenido",
href: "/content",
icon: FileText,
},
{
title: "Ajustes",
href: "/settings",
icon: Settings,
},
{
title: "Permisos",
href: "/permissions",
icon: Shield,
},
];
Componente:
// 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>Menú</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>
);
}
Resaltado: usePathname() + isActive.
3.5 Menú multinivel
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";
<Collapsible defaultOpen>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<Settings className="h-4 w-4" />
<span>Ajustes</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>General</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton href="/settings/security">
<span>Seguridad</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
4. Funciones avanzadas
4.1 RBAC
// lib/navigation.ts
export interface NavItem {
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
roles?: string[];
}
export const navConfig: NavItem[] = [
{
title: "Resumen",
href: "/dashboard",
icon: Home,
},
{
title: "Usuarios",
href: "/users",
icon: Users,
roles: ["admin", "manager"],
},
{
title: "Permisos",
href: "/permissions",
icon: Shield,
roles: ["admin"],
},
];
Filtrado en AppSidebar:
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>
);
}
4.2 Enlaces externos y grupos
<SidebarGroup>
<SidebarGroupLabel>Funciones principales</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{/* ítems principales */}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Ayuda</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<a href="https://docs.example.com" target="_blank" rel="noopener">
<BookOpen className="h-4 w-4" />
<span>Documentación</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>Contacto</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
4.3 Búsqueda (Cmd+K)
import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command";
<SidebarGroup>
<SidebarGroupContent>
<Command className="rounded-lg border shadow-md">
<CommandInput placeholder="Buscar en el menú..." />
<CommandList>
<CommandEmpty>Sin resultados</CommandEmpty>
<CommandGroup heading="Sugerencias">
{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. Rendimiento y buenas prácticas
Server Components primero
AppSidebarcomo"use client"(usePathname)- Header/Footer estáticos como Server Components si puedes
- navConfig generado en servidor y pasado al cliente
Menos JS en el cliente.
Carga diferida de menús grandes
import dynamic from "next/dynamic";
const AdminMenu = dynamic(() => import("./admin-menu"), {
loading: () => <SidebarMenuSkeleton />,
});
En la práctica, pocos paneles tienen decenas de ítems.
Accesibilidad
- Icono y texto
- No ocultes el foco visible
- Prueba Tab y flechas
6. Preguntas frecuentes
¿Se pierde el estado del Sidebar al recargar?
Si SidebarProvider está solo en Dashboard Layout, sí. Súbelo a Root Layout.
¿Cerrar Sidebar en móvil?
const { setOpenMobile } = useSidebar();
<SidebarMenuButton
onClick={() => setOpenMobile(false)}
>
¿Ancho personalizado?
<Sidebar
style={{
"--sidebar-width": "280px",
"--sidebar-width-mobile": "100%",
}}
>
O cambia SIDEBAR_WIDTH en sidebar.tsx.
Resumen
Sidebar de shadcn/ui + Layout de Next.js acelera el esqueleto del panel:
- Entiende Provider, Sidebar, Content, etc.
- Route Groups + SidebarProvider en Root Layout
- navConfig como única fuente de verdad
usePathname+isActiverolespara RBAC
Lo he reutilizado en varios proyectos: nueva página = una entrada en navConfig.
En el próximo artículo hablaré de DataTable con shadcn/ui.
Referencias
Montar esqueleto de panel con Sidebar de shadcn/ui y Layout de Next.js
Construye desde cero un layout de administración extensible con barra lateral, resaltado de rutas y control de permisos
⏱️ Estimated time: 45 min
- 1
Step 1: Instalar shadcn/ui y añadir Sidebar
Inicializa el proyecto y añade el componente:
```bash
npx shadcn@latest init
npx shadcn@latest add sidebar
```
Elige la configuración de estilo por defecto. Tras terminar, tendrás sidebar.tsx en components/ui. - 2
Step 2: Configurar Root Layout
Envuelve la app con SidebarProvider en app/layout.tsx:
• Importa SidebarProvider
• Envuelve {children} dentro de body
• Define lang="es"
Así el estado del Sidebar persiste de forma global. - 3
Step 3: Crear Dashboard Layout
Crea app/(dashboard)/layout.tsx:
• Usa la sintaxis de Route Groups (dashboard)
• Importa AppSidebar y SidebarInset
• Añade Header superior y migas de pan
Los Route Groups no aparecen en la URL: /dashboard sigue siendo la ruta raíz del grupo. - 4
Step 4: Definir configuración de navegación
Crea lib/navigation.ts:
• Interfaz NavItem (title, href, icon, badge)
• Exporta el array navConfig
• Opcional: campo roles para permisos
Añadir una página nueva es una línea en la config. - 5
Step 5: Implementar AppSidebar
Crea components/app-sidebar.tsx:
• Marca "use client"
• usePathname para la ruta actual
• Itera navConfig y renderiza ítems
• isActive cuando la ruta coincide - 6
Step 6: Añadir control de permisos (opcional)
Filtrado RBAC:
• Campo roles en NavItem
• useAuth para el rol del usuario
• filter sobre navConfig
Sin roles, el ítem es visible para todos.
FAQ
¿En qué se diferencia Sidebar de shadcn/ui del de Ant Design?
¿SidebarProvider en Root Layout o en Dashboard Layout?
¿Cómo cerrar el Sidebar en móvil al pulsar un ítem?
```tsx
const { setOpenMobile } = useSidebar();
<SidebarMenuButton onClick={() => setOpenMobile(false)}>
```
El cajón se cierra solo.
¿Cómo personalizar el ancho del Sidebar?
1. Variable CSS (recomendado):
```tsx
<Sidebar style={{ "--sidebar-width": "280px" }} />
```
2. Constante SIDEBAR_WIDTH en sidebar.tsx
La variable CSS permite anchos distintos por instancia.
¿Cómo implementar menú multinivel?
• CollapsibleTrigger para el ítem de primer nivel
• CollapsibleContent con SidebarMenuSub
• Icono ChevronDown para indicar expansión
Código completo en la sección 3.5 del artículo.
¿Qué tal la accesibilidad del Sidebar de shadcn/ui?
7 min de lectura · Publicado el: 27 mar 2026 · Actualizado el: 21 ago 2026
Tailwind y shadcn/ui en práctica
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Guía completa de instalación y personalización de temas en shadcn/ui (con variables CSS)
Instalación y personalización de temas en shadcn/ui: variables CSS, colores OKLCH y modo oscuro. Aprende las mejores prácticas de UI con identidad de marca y configura lo básico en 5 minutos
Parte 4 de 14
Siguiente
Diseño responsive con Tailwind: container queries y estrategia de breakpoints
Container queries y breakpoints en Tailwind CSS: de la ventana al contenedor, y cómo lograr layouts responsive a nivel de componente.
Parte 6 de 14



Comentarios
Inicia sesión con GitHub para dejar un comentario