Sidebar
The appโs main navigation. It collapses on desktop and becomes a sheet on phones.
The documentation navigation is this exact registry Sidebar. Toggle it here or press Cmd/Ctrl+B, or drag its edge to resize.
On phones, it opens the installed Sheet with modal focus management.
When to use it
Use Sidebar for the top level of an app: the sections people move between all day. It keeps their place visible and stays out of the way when collapsed.
For switching views of a single thing, use Tabs. For showing the path to a nested page, add a Breadcrumb.
SidebarThis page
Where you are in the app.
Room to focus
Toggle the live navigation.
The main sections of an app.
Tabs
Views of one thing.
Website refresh
ActiveDesign studio ยท Updated today
Overview, activity, and settings.
How it behaves
Wrap the layout in SidebarProvider. Compose SidebarHeader, SidebarContent, SidebarGroup, SidebarMenu, and SidebarFooter inside Sidebar.
The documentation navigation renders this registry source. Below 768px, Sidebar uses the installed Sheet for modal focus, Escape dismissal, and background interaction locking.
Use collapsible=icon, offcanvas (default), or none. Use SidebarInset as the content sibling when variant=inset; the provider and sidebar must share that layout.
SidebarTrigger and Cmd/Ctrl+B toggle the provider. open/onOpenChange support controlled desktop state; mobile state is independent. Desktop changes write sidebar_state; read it in your application and pass defaultOpen if you need restoration.
SidebarRail toggles on click and resizes on drag between 192px and 384px; dragging well past the minimum collapses the sidebar. Width lives in the provider as --sidebar-width. Drags write sidebar_width; read it and pass defaultWidth (pixels) if you need restoration. Keyboard users toggle with Cmd/Ctrl+B.
All upstream parts remain available, including submenus, actions, badges, skeletons, rail, and menu tooltips. Compose links with render={<a href=... />}. Place TooltipProvider around the application.
Desktop layout changes are immediate. The mobile Sheet retains its short pointer transition and reduced-motion alternative.
Build it
Add it with the shadcn CLI:
1pnpm dlx shadcn@latest add https://jzbui.com/r/sidebar.jsonA complete example. Change the values to fit your product:
12345678910111213141516171819202122232425262728293031import { Home } from "lucide-react"import { TooltipProvider } from "@/components/ui/tooltip"import { Sidebar, SidebarProvider, SidebarHeader, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarFooter, SidebarInset, SidebarRail, SidebarTrigger } from "@/components/ui/sidebar"
<TooltipProvider> <SidebarProvider> <Sidebar variant="inset" collapsible="icon"> <SidebarHeader>Workspace</SidebarHeader> <SidebarContent> <SidebarGroup> <SidebarGroupLabel>Projects</SidebarGroupLabel> <SidebarMenu> <SidebarMenuItem> <SidebarMenuButton render={<a href="/projects" />} isActive tooltip="Projects"> <Home aria-hidden="true" /><span>Projects</span> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarGroup> </SidebarContent> <SidebarFooter>Account</SidebarFooter> <SidebarRail /> </Sidebar> <SidebarInset> <SidebarTrigger /> <h1>Projects</h1> </SidebarInset> </SidebarProvider></TooltipProvider>The full source is yours to edit once installed:
1import { cn, mergeClassName } from "@/registry/lib/utils"2import { mergeProps } from "@base-ui/react/merge-props"3import { useRender } from "@base-ui/react/use-render"4import { cva, type VariantProps } from "class-variance-authority"5import * as React from "react"6 7import { PanelLeftIcon } from "@/registry/lib/icons"8import { useIsMobile } from "@/registry/lib/use-mobile"9import { Button } from "@/registry/ui/button"10import { Input } from "@/registry/ui/input"11import { Separator } from "@/registry/ui/separator"12import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/registry/ui/sheet"13import { Skeleton } from "@/registry/ui/skeleton"14import { Tooltip, TooltipContent, TooltipTrigger } from "@/registry/ui/tooltip"15 16const SIDEBAR_COOKIE_NAME = "sidebar_state"17 18const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 719 20const SIDEBAR_WIDTH_COOKIE_NAME = "sidebar_width"21 22const SIDEBAR_MIN_WIDTH = 19223 24const SIDEBAR_MAX_WIDTH = 38425 26// Dragging this far past the minimum width collapses the sidebar.27const SIDEBAR_COLLAPSE_THRESHOLD = 6428 29const SIDEBAR_WIDTH = "16rem"30 31const SIDEBAR_WIDTH_MOBILE = "18rem"32 33const SIDEBAR_WIDTH_ICON = "3rem"34 35const SIDEBAR_KEYBOARD_SHORTCUT = "b"36 37type SidebarContextProps = {38 state: "expanded" | "collapsed"39 open: boolean40 setOpen: (open: boolean) => void41 openMobile: boolean42 setOpenMobile: (open: boolean) => void43 isMobile: boolean44 toggleSidebar: () => void45 width: number | undefined46 setWidth: (width: number) => void47}48 49const SidebarContext = React.createContext<SidebarContextProps | null>(null)50 51function useSidebar() {52 const context = React.useContext(SidebarContext)53 54 if (!context) {55 throw new Error("useSidebar must be used within a SidebarProvider.")56 }57 58 return context59}60 61function SidebarProvider({62 defaultOpen = true,63 open: openProp,64 onOpenChange: setOpenProp,65 defaultWidth,66 className,67 style,68 children,69 ...props70}: React.ComponentProps<"div"> & {71 defaultOpen?: boolean72 open?: boolean73 onOpenChange?: (open: boolean) => void74 defaultWidth?: number75}) {76 const isMobile = useIsMobile()77 const [openMobile, setOpenMobile] = React.useState(false)78 const [width, setWidth] = React.useState(defaultWidth)79 80 // This is the internal state of the sidebar.81 // We use openProp and setOpenProp for control from outside the component.82 const [_open, _setOpen] = React.useState(defaultOpen)83 const open = openProp ?? _open84 85 const setOpen = React.useCallback(86 (value: boolean | ((value: boolean) => boolean)) => {87 // React state setters accept either a boolean or its typed updater function.88 // oxlint-disable-next-line anti-slop/no-runtime-typeof89 const openState = typeof value === "function" ? value(open) : value90 91 if (setOpenProp) {92 setOpenProp(openState)93 } else {94 _setOpen(openState)95 }96 97 // This sets the cookie to keep the sidebar state.98 document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`99 },100 [setOpenProp, open]101 )102 103 // Helper to toggle the sidebar.104 const toggleSidebar = React.useCallback(() => {105 return isMobile ? setOpenMobile((current) => !current) : setOpen((current) => !current)106 }, [isMobile, setOpen, setOpenMobile])107 108 // Adds a keyboard shortcut to toggle the sidebar.109 React.useEffect(() => {110 const handleKeyDown = (event: KeyboardEvent) => {111 if (112 event.key === SIDEBAR_KEYBOARD_SHORTCUT113 && (event.metaKey || event.ctrlKey)114 ) {115 event.preventDefault()116 toggleSidebar()117 }118 }119 120 window.addEventListener("keydown", handleKeyDown)121 122 return () => window.removeEventListener("keydown", handleKeyDown)123 }, [toggleSidebar])124 125 // We add a state so that we can do data-state="expanded" or "collapsed".126 // This makes it easier to style the sidebar with Tailwind classes.127 const state = open ? "expanded" : "collapsed"128 129 const contextValue = React.useMemo<SidebarContextProps>(130 () => ({131 state,132 open,133 setOpen,134 isMobile,135 openMobile,136 setOpenMobile,137 toggleSidebar,138 width,139 setWidth140 }),141 [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, width]142 )143 144 return (145 <SidebarContext.Provider value={contextValue}>146 <div147 data-slot="sidebar-wrapper"148 style={149 // SAFETY: The object adds string-valued CSS custom properties to React CSSProperties.150 {151 "--sidebar-width": width === undefined ? SIDEBAR_WIDTH : `${width}px`,152 "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,153 ...style154 } as React.CSSProperties155 }156 className={mergeClassName(157 "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",158 className159 )}160 {...props}161 >162 {children}163 </div>164 </SidebarContext.Provider>165 )166}167 168function Sidebar({169 side = "left",170 variant = "sidebar",171 collapsible = "offcanvas",172 className,173 children,174 dir,175 ...props176}: React.ComponentProps<"div"> & {177 side?: "left" | "right"178 variant?: "sidebar" | "floating" | "inset"179 collapsible?: "offcanvas" | "icon" | "none"180}) {181 const { isMobile, state, openMobile, setOpenMobile } = useSidebar()182 183 if (collapsible === "none") {184 return (185 <div186 data-slot="sidebar"187 className={mergeClassName(188 "flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",189 className190 )}191 {...props}192 >193 {children}194 </div>195 )196 }197 198 if (isMobile) {199 return (200 <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>201 <SheetContent202 dir={dir}203 data-sidebar="sidebar"204 data-slot="sidebar"205 data-mobile="true"206 className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"207 style={208 // SAFETY: The object adds string-valued CSS custom properties to React CSSProperties.209 {210 "--sidebar-width": SIDEBAR_WIDTH_MOBILE211 } as React.CSSProperties212 }213 side={side}214 >215 <SheetHeader className="sr-only">216 <SheetTitle>Sidebar</SheetTitle>217 <SheetDescription>Displays the mobile sidebar.</SheetDescription>218 </SheetHeader>219 <div className="flex h-full w-full flex-col">{children}</div>220 </SheetContent>221 </Sheet>222 )223 }224 225 return (226 <div227 className="group peer hidden text-sidebar-foreground md:block"228 data-state={state}229 data-collapsible={state === "collapsed" ? collapsible : ""}230 data-variant={variant}231 data-side={side}232 data-slot="sidebar"233 >234 {/* This is what handles the sidebar gap on desktop */}235 <div236 data-slot="sidebar-gap"237 className={cn(238 "relative w-(--sidebar-width) bg-transparent transition-none",239 "group-data-[collapsible=offcanvas]:w-0",240 "group-data-[side=right]:rotate-180",241 variant === "floating" || variant === "inset"242 ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"243 : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"244 )}245 />246 <div247 data-slot="sidebar-container"248 data-side={side}249 className={mergeClassName(250 cn(251 "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-none data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",252 variant === "floating" || variant === "inset"253 ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"254 : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l"255 ),256 className257 )}258 {...props}259 >260 <div261 data-sidebar="sidebar"262 data-slot="sidebar-inner"263 className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"264 >265 {children}266 </div>267 </div>268 </div>269 )270}271 272function SidebarTrigger({273 className,274 onClick,275 ...props276}: React.ComponentProps<typeof Button>) {277 const { toggleSidebar } = useSidebar()278 279 return (280 <Button281 data-sidebar="trigger"282 data-slot="sidebar-trigger"283 variant="ghost"284 size="icon-sm"285 className={cn(className)}286 onClick={(event) => {287 onClick?.(event)288 toggleSidebar()289 }}290 {...props}291 >292 <PanelLeftIcon />293 <span className="sr-only">Toggle Sidebar</span>294 </Button>295 )296}297 298function SidebarRail({299 className,300 onClick,301 onPointerDown,302 onPointerMove,303 onPointerUp,304 onLostPointerCapture,305 ...props306}: React.ComponentProps<"button">) {307 const { toggleSidebar, setOpen, setWidth, state } = useSidebar()308 const drag = React.useRef<{ x: number; width: number; direction: number; last?: number } | null>(null)309 const suppressClick = React.useRef(false)310 const [resizing, setResizing] = React.useState(false)311 312 function endDrag() {313 const current = drag.current314 315 if (!current) return316 drag.current = null317 setResizing(false)318 319 if (current.last === undefined) return320 // A drag ends with a click on the rail; it should not also toggle the sidebar.321 suppressClick.current = true322 document.cookie =323 `${SIDEBAR_WIDTH_COOKIE_NAME}=${current.last}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`324 }325 326 return (327 <button328 data-sidebar="rail"329 data-slot="sidebar-rail"330 data-resizing={resizing || undefined}331 aria-label="Toggle Sidebar"332 tabIndex={-1}333 onClick={(event) => {334 onClick?.(event)335 336 if (suppressClick.current) {337 suppressClick.current = false338 339 return340 }341 342 toggleSidebar()343 }}344 onPointerDown={(event) => {345 onPointerDown?.(event)346 const container = event.currentTarget.closest<HTMLElement>("[data-slot=sidebar-container]")347 348 suppressClick.current = false349 350 if (event.defaultPrevented || event.button !== 0 || state === "collapsed" || !container) return351 // Prevent text selection while dragging; the click still follows pointerup.352 event.preventDefault()353 event.currentTarget.setPointerCapture(event.pointerId)354 drag.current = {355 x: event.clientX,356 width: container.getBoundingClientRect().width,357 direction: container.dataset.side === "right" ? -1 : 1358 }359 }}360 onPointerMove={(event) => {361 onPointerMove?.(event)362 const current = drag.current363 364 if (!current) return365 const offset = (event.clientX - current.x) * current.direction366 367 if (current.last === undefined && Math.abs(offset) < 3) return368 setResizing(true)369 const next = current.width + offset370 371 if (next < SIDEBAR_MIN_WIDTH - SIDEBAR_COLLAPSE_THRESHOLD) {372 current.last ??= Math.round(current.width)373 endDrag()374 setOpen(false)375 376 return377 }378 379 current.last = Math.round(Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, next)))380 setWidth(current.last)381 }}382 onPointerUp={(event) => {383 onPointerUp?.(event)384 endDrag()385 }}386 onLostPointerCapture={(event) => {387 onLostPointerCapture?.(event)388 endDrag()389 }}390 title="Drag to resize, click to toggle"391 className={mergeClassName(392 cn(393 "absolute inset-y-0 z-20 hidden w-4 transition-[background-color,border-color,color,opacity,transform] ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",394 "cursor-col-resize select-none data-resizing:after:bg-sidebar-border",395 // The inset variant's rail follows the inset panel edge; the panel border already marks it.396 "group-data-[variant=inset]:inset-y-2 group-data-[variant=inset]:after:hidden",397 "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",398 "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",399 "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",400 "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2"401 ),402 className403 )}404 {...props}405 />406 )407}408 409function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {410 return (411 <main412 data-slot="sidebar-inset"413 className={mergeClassName(414 "relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-lg md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",415 className416 )}417 {...props}418 />419 )420}421 422function SidebarInput({423 className,424 ...props425}: React.ComponentProps<typeof Input>) {426 return (427 <Input428 data-slot="sidebar-input"429 data-sidebar="input"430 className={mergeClassName("h-8 w-full bg-background shadow-none", className)}431 {...props}432 />433 )434}435 436function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {437 return (438 <div439 data-slot="sidebar-header"440 data-sidebar="header"441 className={mergeClassName("flex flex-col gap-2 p-2", className)}442 {...props}443 />444 )445}446 447function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {448 return (449 <div450 data-slot="sidebar-footer"451 data-sidebar="footer"452 className={mergeClassName("flex flex-col gap-2 p-2", className)}453 {...props}454 />455 )456}457 458function SidebarSeparator({459 className,460 ...props461}: React.ComponentProps<typeof Separator>) {462 return (463 <Separator464 data-slot="sidebar-separator"465 data-sidebar="separator"466 className={mergeClassName("mx-2 w-auto bg-sidebar-border", className)}467 {...props}468 />469 )470}471 472function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {473 return (474 <div475 data-slot="sidebar-content"476 data-sidebar="content"477 className={mergeClassName(478 "flex overscroll-none min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden",479 className480 )}481 {...props}482 />483 )484}485 486function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {487 return (488 <div489 data-slot="sidebar-group"490 data-sidebar="group"491 className={mergeClassName("relative flex w-full min-w-0 flex-col p-2", className)}492 {...props}493 />494 )495}496 497function SidebarGroupLabel({498 className,499 render,500 ...props501}: useRender.ComponentProps<"div"> & React.ComponentProps<"div">) {502 return useRender({503 defaultTagName: "div",504 props: mergeProps<"div">(505 {506 className: mergeClassName(507 "flex h-8 shrink-0 items-center rounded-md px-2 label-sm text-sidebar-foreground ring-sidebar-ring outline-hidden transition-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",508 className509 )510 },511 props512 ),513 render,514 state: {515 slot: "sidebar-group-label",516 sidebar: "group-label"517 }518 })519}520 521function SidebarGroupAction({522 className,523 render,524 ...props525}: useRender.ComponentProps<"button"> & React.ComponentProps<"button">) {526 return useRender({527 defaultTagName: "button",528 props: mergeProps<"button">(529 {530 className: mergeClassName(531 "absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",532 className533 )534 },535 props536 ),537 render,538 state: {539 slot: "sidebar-group-action",540 sidebar: "group-action"541 }542 })543}544 545function SidebarGroupContent({546 className,547 ...props548}: React.ComponentProps<"div">) {549 return (550 <div551 data-slot="sidebar-group-content"552 data-sidebar="group-content"553 className={mergeClassName("w-full body-sm", className)}554 {...props}555 />556 )557}558 559function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {560 return (561 <ul562 data-slot="sidebar-menu"563 data-sidebar="menu"564 className={mergeClassName("flex w-full min-w-0 flex-col gap-0", className)}565 {...props}566 />567 )568}569 570function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {571 return (572 <li573 data-slot="sidebar-menu-item"574 data-sidebar="menu-item"575 className={mergeClassName("group/menu-item relative", className)}576 {...props}577 />578 )579}580 581const sidebarMenuButtonVariants = cva(582 "peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left body-sm ring-sidebar-ring outline-hidden transition-none group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:label-md data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",583 {584 variants: {585 variant: {586 default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",587 outline:588 "bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]"589 },590 size: {591 default: "h-8 body-sm",592 sm: "h-7 caption",593 lg: "h-12 body-sm group-data-[collapsible=icon]:p-0!"594 }595 },596 defaultVariants: {597 variant: "default",598 size: "default"599 }600 }601)602 603function SidebarMenuButton({604 render,605 isActive = false,606 variant = "default",607 size = "default",608 tooltip,609 className,610 ...props611}:612 & useRender.ComponentProps<"button">613 & React.ComponentProps<"button">614 & {615 isActive?: boolean616 tooltip?: string | React.ComponentProps<typeof TooltipContent>617 }618 & VariantProps<typeof sidebarMenuButtonVariants>)619{620 const { isMobile, state } = useSidebar()621 622 const comp = useRender({623 defaultTagName: "button",624 props: mergeProps<"button">(625 {626 className: mergeClassName(sidebarMenuButtonVariants({ variant, size }), className)627 },628 props629 ),630 render: !tooltip ? render : <TooltipTrigger render={render} />,631 state: {632 slot: "sidebar-menu-button",633 sidebar: "menu-button",634 size,635 active: isActive636 }637 })638 639 if (!tooltip) {640 return comp641 }642 643 // The public Tooltip prop accepts a string shorthand or known TooltipContent props.644 // oxlint-disable-next-line anti-slop/no-runtime-typeof645 if (typeof tooltip === "string") {646 tooltip = {647 children: tooltip648 }649 }650 651 return (652 <Tooltip>653 {comp}654 <TooltipContent655 side="right"656 align="center"657 hidden={state !== "collapsed" || isMobile}658 {...tooltip}659 />660 </Tooltip>661 )662}663 664function SidebarMenuAction({665 className,666 render,667 showOnHover = false,668 ...props669}:670 & useRender.ComponentProps<"button">671 & React.ComponentProps<"button">672 & {673 showOnHover?: boolean674 })675{676 return useRender({677 defaultTagName: "button",678 props: mergeProps<"button">(679 {680 className: mergeClassName(681 cn(682 "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",683 showOnHover684 && "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0"685 ),686 className687 )688 },689 props690 ),691 render,692 state: {693 slot: "sidebar-menu-action",694 sidebar: "menu-action"695 }696 })697}698 699function SidebarMenuBadge({700 className,701 ...props702}: React.ComponentProps<"div">) {703 return (704 <div705 data-slot="sidebar-menu-badge"706 data-sidebar="menu-badge"707 className={mergeClassName(708 "pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 label-sm text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground",709 className710 )}711 {...props}712 />713 )714}715 716function SidebarMenuSkeleton({717 className,718 showIcon = false,719 ...props720}: React.ComponentProps<"div"> & {721 showIcon?: boolean722}) {723 // Random width between 50 to 90%.724 const [width] = React.useState(() => {725 return `${Math.floor(Math.random() * 40) + 50}%`726 })727 728 return (729 <div730 data-slot="sidebar-menu-skeleton"731 data-sidebar="menu-skeleton"732 className={mergeClassName("flex h-8 items-center gap-2 rounded-md px-2", className)}733 {...props}734 >735 {showIcon && (736 <Skeleton737 className="size-4 rounded-md"738 data-sidebar="menu-skeleton-icon"739 />740 )}741 <Skeleton742 className="h-4 max-w-(--skeleton-width) flex-1"743 data-sidebar="menu-skeleton-text"744 style={745 // SAFETY: The object adds string-valued CSS custom properties to React CSSProperties.746 {747 "--skeleton-width": width748 } as React.CSSProperties749 }750 />751 </div>752 )753}754 755function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {756 return (757 <ul758 data-slot="sidebar-menu-sub"759 data-sidebar="menu-sub"760 className={mergeClassName(761 "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",762 className763 )}764 {...props}765 />766 )767}768 769function SidebarMenuSubItem({770 className,771 ...props772}: React.ComponentProps<"li">) {773 return (774 <li775 data-slot="sidebar-menu-sub-item"776 data-sidebar="menu-sub-item"777 className={mergeClassName("group/menu-sub-item relative", className)}778 {...props}779 />780 )781}782 783function SidebarMenuSubButton({784 render,785 size = "md",786 isActive = false,787 className,788 ...props789}:790 & useRender.ComponentProps<"a">791 & React.ComponentProps<"a">792 & {793 size?: "sm" | "md"794 isActive?: boolean795 })796{797 return useRender({798 defaultTagName: "a",799 props: mergeProps<"a">(800 {801 className: mergeClassName(802 "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:body-sm data-[size=sm]:caption data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",803 className804 )805 },806 props807 ),808 render,809 state: {810 slot: "sidebar-menu-sub-button",811 sidebar: "menu-sub-button",812 size,813 active: isActive814 }815 })816}817 818export {819 Sidebar,820 SidebarContent,821 SidebarFooter,822 SidebarGroup,823 SidebarGroupAction,824 SidebarGroupContent,825 SidebarGroupLabel,826 SidebarHeader,827 SidebarInput,828 SidebarInset,829 SidebarMenu,830 SidebarMenuAction,831 SidebarMenuBadge,832 SidebarMenuButton,833 SidebarMenuItem,834 SidebarMenuSkeleton,835 SidebarMenuSub,836 SidebarMenuSubButton,837 SidebarMenuSubItem,838 SidebarProvider,839 SidebarRail,840 SidebarSeparator,841 SidebarTrigger,842 useSidebar843}844