Sidebar
A composable, themeable and customizable sidebar component.
Installation
npx shadcn@latest add @uqudo/sidebarInstalling it also installs the pieces it is built from: Button, Input, Separator, Sheet (the mobile drawer), Skeleton, Tooltip and the useIsMobile hook.
The sidebar is colored through the --sidebar-* CSS variables. They are
defined in this project's app/globals.css; make sure your own stylesheet
declares them (for both light and dark) or the sidebar renders unstyled:
:root {
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.546 0.245 262.881);
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
@theme inline {
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}Usage
In a real app the desktop sidebar is fixed to the viewport edge, which is
why the previews on this page use collapsible="none". The structure is
always the same: a SidebarProvider around the whole page, a Sidebar, and a
SidebarInset for the page content.
import { SidebarProvider, SidebarTrigger, SidebarInset } from "@/components/uqudo/ui/sidebar"
import { AppSidebar } from "@/components/app-sidebar"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-12 items-center gap-2 px-4">
<SidebarTrigger />
</header>
{children}
</SidebarInset>
</SidebarProvider>
)
}import { CalendarIcon, HomeIcon, InboxIcon } from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/uqudo/ui/sidebar"
const items = [
{ title: "Home", url: "/", icon: HomeIcon },
{ title: "Inbox", url: "/inbox", icon: InboxIcon },
{ title: "Calendar", url: "/calendar", icon: CalendarIcon },
]
export function AppSidebar() {
return (
<Sidebar>
<SidebarHeader />
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild tooltip={item.title}>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter />
</Sidebar>
)
}Below the md breakpoint the sidebar renders inside a
Sheet that SidebarTrigger opens. On desktop the
trigger, the SidebarRail and the ⌘B / Ctrl+B shortcut toggle it between
expanded and collapsed.
Collapsible modes
<Sidebar collapsible="offcanvas" /> // slides out of view (default)
<Sidebar collapsible="icon" /> // shrinks to a 3rem icon rail
<Sidebar collapsible="none" /> // always expanded, plain flex childWith collapsible="icon", group labels, badges, actions and sub-menus hide
automatically and SidebarMenuButton's tooltip shows on hover.
Variants and side
<Sidebar variant="sidebar" /> // flush against the edge (default)
<Sidebar variant="floating" /> // padded, rounded card with a ring
<Sidebar variant="inset" /> // padded; SidebarInset gets margin and rounded corners
<Sidebar side="right" />Persisting the state
SidebarProvider writes the open state to a sidebar_state cookie (7 days).
Read it in a server layout to avoid a flash on reload:
import { cookies } from "next/headers"
export default async function Layout({ children }: { children: React.ReactNode }) {
const cookieStore = await cookies()
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
return (
<SidebarProvider defaultOpen={defaultOpen}>
<AppSidebar />
<SidebarInset>{children}</SidebarInset>
</SidebarProvider>
)
}Width
The provider sets --sidebar-width (16rem) and --sidebar-width-icon (3rem)
as inline styles; override them through style. The mobile sheet uses its own
--sidebar-width-mobile (18rem).
<SidebarProvider
style={{
"--sidebar-width": "20rem",
"--sidebar-width-mobile": "20rem",
} as React.CSSProperties}
>useSidebar
Any component under the provider can read and drive the state:
"use client"
import { useSidebar } from "@/components/uqudo/ui/sidebar"
export function CollapseButton() {
const { state, open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar } = useSidebar()
return <button onClick={toggleSidebar}>{state === "expanded" ? "Collapse" : "Expand"}</button>
}Examples
Sub-menus
Nest a SidebarMenuSub inside a SidebarMenuItem for a second level of
links. Sub-menus are hidden when the sidebar collapses to icons.
API Reference
SidebarProvider
Prop
Type
Sidebar
Prop
Type
SidebarMenuButton
Prop
Type
SidebarMenuSubButton
Prop
Type
useSidebar
Throws when called outside a SidebarProvider.
Prop
Type
Other parts
| Component | Renders | Notes |
|---|---|---|
SidebarTrigger | Button | Ghost icon button that calls toggleSidebar. Accepts Button props. |
SidebarRail | <button> | Thin clickable strip on the sidebar edge that toggles it. |
SidebarInset | <main> | Page content; adapts its margin and radius to the inset variant. |
SidebarInput | Input | Search field styled for the sidebar. |
SidebarHeader | <div> | Top area, p-2. |
SidebarFooter | <div> | Bottom area, p-2. |
SidebarSeparator | Separator | Inset divider using --sidebar-border. |
SidebarContent | <div> | Scrollable middle area. |
SidebarGroup | <div> | A section inside the content. |
SidebarGroupLabel | <div> | Section heading. Hidden in icon mode. Supports asChild. |
SidebarGroupAction | <button> | Small action in the group's top end corner. Supports asChild. |
SidebarGroupContent | <div> | Wraps the group's menu. |
SidebarMenu | <ul> | List of SidebarMenuItem. |
SidebarMenuItem | <li> | One row; positions actions and badges. |
SidebarMenuAction | <button> | Trailing action on a row. showOnHover reveals it on hover/focus. |
SidebarMenuBadge | <div> | Trailing count on a row. |
SidebarMenuSkeleton | <div> | Loading row with a random-width Skeleton. showIcon adds a square. |
SidebarMenuSub | <ul> | Indented second-level list. |
SidebarMenuSubItem | <li> | One sub-row. |
Every part carries a data-slot and a data-sidebar attribute named after
it, and the desktop Sidebar exposes data-state, data-collapsible,
data-variant and data-side for styling descendants with
group-data-[...] utilities.