Sidebar
Componente Sidebar de shadcn — navegación responsive con secciones colapsables, drawer móvil y state persistente.
Receta
Tarjeta de receta de referencia rápida — lista para copiar y pegar.
npx shadcn@latest add sidebarimport {
Sidebar, SidebarContent, SidebarFooter, SidebarGroup,
SidebarGroupContent, SidebarGroupLabel, SidebarHeader,
SidebarMenu, SidebarMenuButton, SidebarMenuItem,
SidebarProvider, SidebarTrigger,
} from "@/components/ui/sidebar";
export function AppLayout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<Sidebar>
<SidebarHeader>
<h2 className="px-4 text-lg font-bold">Mi App</h2>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Principal</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<a href="/">Panel</a>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<a href="/settings">Configuración</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<p className="px-4 text-xs text-muted-foreground">v1.0.0</p>
</SidebarFooter>
</Sidebar>
<main className="flex-1">
<SidebarTrigger className="m-4" />
{children}
</main>
</SidebarProvider>
);
}Cuándo usarlo: Cuando tu app necesita navegación persistente — dashboards, paneles de administración, sitios de documentación o cualquier aplicación multipágina.
Ejemplo funcional
"use client";
import { usePathname } from "next/navigation";
import Link from "next/link";
import {
Sidebar, SidebarContent, SidebarFooter, SidebarGroup,
SidebarGroupContent, SidebarGroupLabel, SidebarHeader,
SidebarMenu, SidebarMenuButton, SidebarMenuItem,
SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem,
SidebarProvider, SidebarTrigger, useSidebar,
} from "@/components/ui/sidebar";
import {
Collapsible, CollapsibleContent, CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
LayoutDashboard, Users, Settings, FileText,
ChevronDown, LogOut, ChevronsUpDown,
} from "lucide-react";
type NavItem = {
title: string;
href: string;
icon: React.ReactNode;
children?: { title: string; href: string }[];
};
const navItems: NavItem[] = [
{ title: "Panel", href: "/dashboard", icon: <LayoutDashboard className="h-4 w-4" /> },
{
title: "Usuarios",
href: "/users",
icon: <Users className="h-4 w-4" />,
children: [
{ title: "Todos los usuarios", href: "/users" },
{ title: "Roles", href: "/users/roles" },
{ title: "Invitaciones", href: "/users/invitations" },
],
},
{
title: "Contenido",
href: "/content",
icon: <FileText className="h-4 w-4" />,
children: [
{ title: "Publicaciones", href: "/content/posts" },
{ title: "Páginas", href: "/content/pages" },
{ title: "Medios", href: "/content/media" },
],
},
{ title: "Configuración", href: "/settings", icon: <Settings className="h-4 w-4" /> },
];
function AppSidebar() {
const pathname = usePathname();
return (
<Sidebar collapsible="icon">
<SidebarHeader className="border-b px-4 py-3">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground text-sm font-bold">
A
</div>
<span className="font-semibold group-data-[collapsible=icon]:hidden">
Acme Inc
</span>
</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navegación</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{navItems.map((item) =>
item.children ? (
<Collapsible key={item.title} defaultOpen={pathname.startsWith(item.href)}>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
{item.icon}
<span>{item.title}</span>
<ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.children.map((child) => (
<SidebarMenuSubItem key={child.href}>
<SidebarMenuSubButton
asChild
isActive={pathname === child.href}
>
<Link href={child.href}>{child.title}</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
) : (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild isActive={pathname === item.href}>
<Link href={item.href}>
{item.icon}
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="border-t">
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton className="w-full">
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-muted text-xs">
JD
</div>
<span className="group-data-[collapsible=icon]:hidden">John Doe</span>
<ChevronsUpDown className="ml-auto h-4 w-4 group-data-[collapsible=icon]:hidden" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start" className="w-48">
<DropdownMenuItem>Perfil</DropdownMenuItem>
<DropdownMenuItem>Facturación</DropdownMenuItem>
<DropdownMenuItem>
<LogOut className="mr-2 h-4 w-4" /> Cerrar sesión
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
);
}
export function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<AppSidebar />
<main className="flex-1 overflow-auto">
<header className="flex items-center gap-4 border-b px-6 py-3">
<SidebarTrigger />
<h1 className="text-lg font-semibold">Panel</h1>
</header>
<div className="p-6">{children}</div>
</main>
</SidebarProvider>
);
}Qué demuestra esto:
- Sidebar completo de dashboard con header, grupos de contenido y footer
- Submenús colapsables que se abren automáticamente según la ruta actual
- Resaltado
isActivepara la página actual - Modo de colapso a iconos (
collapsible="icon") — el sidebar se reduce a solo iconos - Menú de usuario desplegable en el footer
- Botón de alternancia
SidebarTrigger - Integración con
Linkde Next.js medianteasChild
Profundización
Cómo funciona
SidebarProvidergestiona el state abierto/cerrado y lo provee mediante contextSidebarrenderiza un<aside>con ancho fijo y comportamiento responsive- En móvil (por debajo de
md), el sidebar se convierte en un drawer deslizable collapsible="icon"reduce el sidebar para mostrar solo iconos, ocultando el texto mediantegroup-data-[collapsible=icon]:hidden- El hook
useSidebar()exponestate,open,setOpen,toggleSidebareisMobile - El state se persiste en una cookie por defecto (configurable)
SidebarMenuButtongestiona el state activo, tooltip en modo colapsado y navegación por teclado
Variaciones
State controlado del sidebar:
"use client";
import { useSidebar } from "@/components/ui/sidebar";
function SidebarToggle() {
const { open, setOpen, toggleSidebar, isMobile } = useSidebar();
return (
<button onClick={toggleSidebar}>
{open ? "Contraer" : "Expandir"}
</button>
);
}Sidebar con badges:
<SidebarMenuButton>
<Users className="h-4 w-4" />
<span>Usuarios</span>
<span className="ml-auto rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground">
12
</span>
</SidebarMenuButton>Sidebar con búsqueda:
<SidebarHeader>
<div className="px-2 py-2">
<Input placeholder="Buscar..." className="h-8" />
</div>
</SidebarHeader>Sidebar en el lado derecho:
<SidebarProvider>
<main className="flex-1">{children}</main>
<Sidebar side="right" collapsible="none">
{/* Panel de propiedades, chat, etc. */}
</Sidebar>
</SidebarProvider>Notas de TypeScript
// Tipo de retorno del hook useSidebar
const sidebar: {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
} = useSidebar();
// Tipo de ítem de navegación
type NavItem = {
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
badge?: number;
children?: Omit<NavItem, "icon" | "children">[];
};Errores comunes
-
SidebarProviderdebe envolver tanto Sidebar como el contenido principal — El provider gestiona el layout. Colocarlo mal rompe el contenedor flex. Solución: Envuélvelo a nivel de layout. -
Comportamiento móvil vs escritorio — En móvil, el sidebar se renderiza como un drawer superpuesto; en escritorio, es inline. Si renderizas condicionalmente según
isMobile, asegúrate de gestionar la hidratación. -
collapsible="icon"necesita iconos — Si tus ítems de menú no tienen iconos, el modo colapsado muestra espacio vacío. Solución: Incluye siempre un icono enSidebarMenuButton. -
Persistencia basada en cookies — La persistencia del state por defecto usa cookies, lo que requiere un layout renderizado en el servidor. Solución: En SPAs, pasa
defaultOpenaSidebarProvidero gestiona el state tú mismo. -
asChilden SidebarMenuButton — Al usar<Link>de Next.js, envuélvelo conasChildpara que el botón del menú delegue el renderizado al Link. SinasChild, obtienes un button envolviendo un anchor (HTML inválido).
Alternativas
| Alternativa | Úsala cuando | No la uses cuando |
|---|---|---|
| Sidebar personalizado | Necesitas un comportamiento no cubierto por el componente shadcn | El Sidebar de shadcn cubre tus necesidades |
| Navegación por pestañas | App mobile-first con pestañas inferiores | Dashboard de escritorio con muchos ítems de navegación |
| Solo navbar superior | Tu app tiene menos de 6 páginas de primer nivel | Necesitas navegación anidada de varios niveles |
| Drawer (Sheet) | Quieres un panel deslizable temporal, no navegación persistente | Necesitas navegación siempre visible |
FAQs
¿Cuál es la estructura mínima necesaria para un Sidebar de shadcn?
SidebarProviderenvolviendo tantoSidebarcomo el contenido demainSidebarconSidebarContentque contengaSidebarGroup,SidebarMenuySidebarMenuItemSidebarTriggeren algún lugar del área principal para alternar el sidebar
¿Qué hace collapsible="icon"?
- Reduce el sidebar para mostrar solo iconos, ocultando las etiquetas de texto
- Los elementos de texto se ocultan mediante el selector CSS
group-data-[collapsible=icon]:hidden - Los ítems del menú deben incluir iconos o el modo colapsado muestra espacio vacío
¿Cómo se comporta el sidebar de forma distinta en móvil vs escritorio?
- Escritorio: se renderiza inline como un
<aside>con ancho fijo - Móvil (por debajo del breakpoint
md): se renderiza como un drawer deslizable superpuesto - Usa
isMobiledeuseSidebar()para detectar el modo actual
¿Cómo construir submenús colapsables que se abran automáticamente según la ruta actual?
<Collapsible defaultOpen={pathname.startsWith(item.href)}>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>{item.icon} {item.title}</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.children.map((child) => (
<SidebarMenuSubItem key={child.href}>
<SidebarMenuSubButton asChild isActive={pathname === child.href}>
<Link href={child.href}>{child.title}</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>¿Qué devuelve el hook useSidebar()?
state:"expanded"o"collapsed"open/setOpen: state booleano abierto y su setteropenMobile/setOpenMobile: state específico para móvilisMobile: si el viewport está por debajo del breakpoint móviltoggleSidebar: alterna entre abierto y cerrado
Trampa: ¿Por qué SidebarProvider debe envolver tanto el Sidebar como el contenido principal?
- El provider gestiona el layout mediante un contenedor flex
- Colocarlo solo alrededor del Sidebar rompe la relación flex con
main - Solución: envuélvelo a nivel de layout para que Sidebar y main sean hermanos
¿Cómo se persiste el state del sidebar entre navegaciones de página?
- Por defecto, el state abierto/cerrado se guarda en una cookie
- Esto requiere un layout renderizado en el servidor para leer la cookie en la carga inicial
- En SPAs sin SSR, pasa
defaultOpenaSidebarProvidero gestiona el state tú mismo
¿Cómo renderizar un sidebar en el lado derecho de la página?
<SidebarProvider>
<main className="flex-1">{children}</main>
<Sidebar side="right" collapsible="none">
{/* Panel de propiedades, chat, etc. */}
</Sidebar>
</SidebarProvider>Trampa: ¿Por qué usar Link de Next.js sin asChild en SidebarMenuButton produce HTML inválido?
- Sin
asChild, SidebarMenuButton renderiza un<button>y tu<Link>renderiza un<a>dentro - Un
<button>envolviendo un<a>es HTML inválido - Solución: usa siempre
asChildcuando el hijo sea un elemento<Link>o<a>
¿Cómo tipar un ítem de navegación para un sidebar con hijos opcionales?
type NavItem = {
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
badge?: number;
children?: Omit<NavItem, "icon" | "children">[];
};¿Cómo agregar un menú de usuario desplegable en el footer del sidebar?
- Coloca un
DropdownMenudentro deSidebarFooter>SidebarMenu>SidebarMenuItem - Usa
SidebarMenuButtoncomoDropdownMenuTriggerconasChild - Configura
side="top"yalign="start"enDropdownMenuContentpara que se abra hacia arriba