# Sidebar-2

> A sidebar built for developer app dashboards

A shadcn/ui block from the [@hexui](https://hexui.sh) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/hexui/sidebar-2).

- Page, with a live preview: https://shadcn.exchange/r/hexui/sidebar-2
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/hexui/sidebar-2-light-98659e9830d19ab3.png
- Registry item: https://hexui.sh/r/sidebar-2.json
- Categories: sidebar
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://hexui.sh/r/sidebar-2.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `avatar`, `button`, `dropdown-menu`, `sidebar`, `tooltip`

## Files

### `registry/blocks/sidebar/sidebar-2/source/app/sidebar/page.tsx`

```tsx
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { AppHeader } from "../../components/app-header"
import { AppSidebar } from "../../components/app-sidebar"

// Customize the expanded sidebar width via the `--sidebar-width` CSS variable.
const SIDEBAR_PROVIDER_STYLE = {
    "--sidebar-width": "14rem",
} as React.CSSProperties

export default function Page() {
    return (
        <SidebarProvider defaultOpen={false} className="min-h-0 flex-1" style={SIDEBAR_PROVIDER_STYLE}>
            <div className="flex min-h-svh w-full flex-col">
                <AppHeader />

                <div className="flex min-h-0 flex-1">
                    <AppSidebar />

                    <SidebarInset className="bg-sidebar">
                        {/* This area acts as a placeholder canvas for the page content preview. */}
                        <div className="flex flex-1 flex-col p-4">
                            <div className="min-h-screen flex-1 rounded-xl bg-muted/50 md:min-h-min" />
                        </div>
                    </SidebarInset>
                </div>
            </div>
        </SidebarProvider>
    )
}
```

### `registry/blocks/sidebar/sidebar-2/source/components/app-brand.tsx`

```tsx
import { Command } from "lucide-react"

type AppBrandProps = {
    href?: string
}

export function AppBrand({ href = "#" }: AppBrandProps) {
    return (
        // Centralize the brand UI so header and sidebar stay visually consistent.
        <a href={href} className="flex items-center gap-2 font-medium">
            <Command className="size-5" />
            <span>Acme Inc.</span>
        </a>
    )
}
```

### `registry/blocks/sidebar/sidebar-2/source/components/app-header.tsx`

```tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { SidebarTrigger } from "@/components/ui/sidebar"
import { AppBrand } from "./app-brand"

export function AppHeader() {
    return (
        <header className="flex h-12 w-full shrink-0 items-center gap-3 border-b border-sidebar-border bg-sidebar px-2 md:px-4">
            <SidebarTrigger className="md:hidden" />
            <AppBrand />

            <div className="ml-auto flex items-center gap-1">
                <Avatar className="size-7">
                    <AvatarImage src="https://avatars.githubusercontent.com/u/114809507" alt="User avatar" />
                    <AvatarFallback>R</AvatarFallback>
                </Avatar>
            </div>
        </header>
    )
}
```

### `registry/blocks/sidebar/sidebar-2/source/components/app-sidebar.tsx`

```tsx
"use client"

import * as React from "react"
import { Button } from "@/components/ui/button"
import {
    DropdownMenu,
    DropdownMenuContent,
    DropdownMenuGroup,
    DropdownMenuLabel,
    DropdownMenuRadioGroup,
    DropdownMenuRadioItem,
    DropdownMenuSeparator,
    DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
    Sidebar,
    SidebarContent,
    SidebarFooter,
    SidebarHeader,
    SidebarMenu,
    SidebarMenuItem,
    useSidebar,
} from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
import {
    ChartArea,
    FolderKanban,
    House,
    Inbox,
    PanelLeftDashed,
    ReceiptText,
    Users,
} from "lucide-react"
import { AppBrand } from "./app-brand"
import { NavMain, type NavMainItem } from "./nav-main"

type SidebarMode = "expanded" | "collapsed" | "hover"

type SidebarModeOption = {
    value: SidebarMode
    label: string
}

const SIDEBAR_MODE_OPTIONS = [
    { value: "expanded", label: "Expanded" },
    { value: "collapsed", label: "Collapsed" },
    { value: "hover", label: "Expand on hover" },
] as const satisfies SidebarModeOption[]

const NAV_ITEMS = [
    { title: "Overview", url: "#", icon: House, isActive: true },
    { title: "Inbox", url: "#", icon: Inbox },
    { title: "Projects", url: "#", icon: FolderKanban },
    { title: "Team", url: "#", icon: Users },
    { title: "Billing", url: "#", icon: ReceiptText },
    { title: "Analytics", url: "#", icon: ChartArea },
] satisfies NavMainItem[]

// In hover mode, a negative right margin lets the expanded panel overlap adjacent content
// instead of pushing it, so the surrounding layout doesn't shift on hover.
function getSidebarShellClassName(mode: SidebarMode, isExpandedInHoverMode: boolean) {
    if (mode !== "hover") {
        return "w-full"
    }

    return isExpandedInHoverMode
        ? "md:w-(--sidebar-width) md:-mr-[calc(var(--sidebar-width)-var(--sidebar-width-icon))]"
        : "w-full"
}

export function AppSidebar() {
    const { open, setOpen } = useSidebar()
    const [mode, setMode] = React.useState<SidebarMode>("hover")
    const [isHovering, setIsHovering] = React.useState(false)
    const [isModeMenuOpen, setIsModeMenuOpen] = React.useState(false)

    // Keep the current label in sync with the selected mode for accessibility.
    const activeMode =
        SIDEBAR_MODE_OPTIONS.find((option) => option.value === mode) ?? SIDEBAR_MODE_OPTIONS[2]

    const isHoverMode = mode === "hover"
    const isExpandedInHoverMode = isHoverMode && (open || isModeMenuOpen)

    // In hover mode, the shell temporarily grows so the expanded panel can overlap content cleanly.
    const sidebarShellClassName = getSidebarShellClassName(mode, isExpandedInHoverMode)

    // Synchronize the shared sidebar state whenever the interaction mode changes.
    React.useEffect(() => {
        if (mode === "expanded") {
            setOpen(true)
            return
        }

        if (mode === "collapsed") {
            setOpen(false)
            return
        }

        setOpen(isHovering || isModeMenuOpen)
    }, [isHovering, isModeMenuOpen, mode, setOpen])

    const handleMouseEnter = () => {
        setIsHovering(true)

        if (isHoverMode) {
            setOpen(true)
        }
    }

    // Do not close the sidebar while the mode dropdown is still open.
    const handleMouseLeave = () => {
        setIsHovering(false)

        if (isHoverMode && !isModeMenuOpen) {
            setOpen(false)
        }
    }

    const handleModeMenuOpenChange = (nextOpen: boolean) => {
        setIsModeMenuOpen(nextOpen)

        if (isHoverMode) {
            setOpen(nextOpen || isHovering)
        }
    }

    return (
        <div
            className={cn(
                // Reserve horizontal space based on the desktop sidebar mode.
                "w-0 shrink-0",
                mode === "expanded" ? "md:w-(--sidebar-width)" : "md:w-(--sidebar-width-icon)"
            )}
        >
            <div
                className={cn("relative", sidebarShellClassName)}
                onMouseEnter={handleMouseEnter}
                onMouseLeave={handleMouseLeave}
            >
                <Sidebar
                    collapsible="icon"
                    className={cn("top-12 h-[calc(100svh-3rem)] border-r", isHoverMode && "z-20")}
                >
                    {/* Mobile shows the brand inside the drawer because the desktop header is hidden there. */}
                    <SidebarHeader className="px-3.5 pt-4 font-medium md:hidden">
                        <AppBrand />
                    </SidebarHeader>

                    <SidebarContent className="px-2 pt-2">
                        <NavMain items={NAV_ITEMS} />
                    </SidebarContent>

                    <SidebarFooter className="hidden border-t border-sidebar-border md:block">
                        <SidebarMenu>
                            <SidebarMenuItem>
                                <DropdownMenu onOpenChange={handleModeMenuOpenChange}>
                                    <DropdownMenuTrigger
                                        render={
                                            <Button
                                                variant="ghost"
                                                size="icon"
                                                className="text-muted-foreground"
                                                aria-label={`Sidebar Control: ${activeMode.label}`}
                                            />
                                        }
                                    >
                                        <PanelLeftDashed />
                                    </DropdownMenuTrigger>

                                    <DropdownMenuContent side="top" align="end" className="ml-3 w-48">
                                        <DropdownMenuGroup>
                                            <DropdownMenuLabel>Sidebar Control</DropdownMenuLabel>
                                        </DropdownMenuGroup>
                                        <DropdownMenuSeparator />
                                        <DropdownMenuRadioGroup
                                            value={mode}
                                            onValueChange={(value) => setMode(value as SidebarMode)}
                                        >
                                            {SIDEBAR_MODE_OPTIONS.map((option) => (
                                                <DropdownMenuRadioItem key={option.value} value={option.value}>
                                                    <span className="text-xs">{option.label}</span>
                                                </DropdownMenuRadioItem>
                                            ))}
                                        </DropdownMenuRadioGroup>
                                    </DropdownMenuContent>
                                </DropdownMenu>
                            </SidebarMenuItem>
                        </SidebarMenu>
                    </SidebarFooter>
                </Sidebar>
            </div>
        </div>
    )
}
```

### `registry/blocks/sidebar/sidebar-2/source/components/nav-main.tsx`

```tsx
import { type LucideIcon } from "lucide-react"
import {
    SidebarGroup,
    SidebarGroupContent,
    SidebarMenu,
    SidebarMenuButton,
    SidebarMenuItem,
} from "@/components/ui/sidebar"

export type NavMainItem = {
    title: string
    url: string
    isActive?: boolean
    icon?: LucideIcon
    emoji?: string
}

type NavMainProps = {
    items: NavMainItem[]
}

export function NavMain({ items }: NavMainProps) {
    return (
        <SidebarGroup className="p-0">
            <SidebarGroupContent>
                <SidebarMenu className="gap-0.5">
                    {items.map((item) => (
                        <SidebarMenuItem key={item.title}>
                            <SidebarMenuButton isActive={item.isActive} tooltip={item.title}>
                                {/* Support both icons and emoji so the same nav renderer can be reused for different demos. */}
                                <a className="flex items-center gap-2" href={item.url}>
                                    {item.icon ? <item.icon className="size-4" /> : null}
                                    {item.emoji ? (
                                        <span aria-hidden="true" className="text-[14px] leading-none">
                                            {item.emoji}
                                        </span>
                                    ) : null}
                                    <span>{item.title}</span>
                                </a>
                            </SidebarMenuButton>
                        </SidebarMenuItem>
                    ))}
                </SidebarMenu>
            </SidebarGroupContent>
        </SidebarGroup>
    )
}
```
