Uqudo UIregistry

Sidebar

A composable, themeable and customizable sidebar component.

UqudoEnterprise
Application
  • 24
Pedro Duartepedro@example.com

Installation

npx shadcn@latest add @uqudo/sidebar

Installing 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.

app/layout.tsx
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>
  )
}
components/app-sidebar.tsx
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 child

With 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:

app/layout.tsx
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

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

Prop

Type

SidebarMenuButton

Prop

Type

SidebarMenuSubButton

Prop

Type

useSidebar

Throws when called outside a SidebarProvider.

Prop

Type

Other parts

ComponentRendersNotes
SidebarTriggerButtonGhost 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.
SidebarInputInputSearch field styled for the sidebar.
SidebarHeader<div>Top area, p-2.
SidebarFooter<div>Bottom area, p-2.
SidebarSeparatorSeparatorInset 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.

On this page