hexui
blocksidebar-2source: ok · refreshed
Sidebar-2
A sidebar built for developer app dashboards
npx shadcn add "https://hexui.sh/r/sidebar-2.json"free
Where it goes
sidebardashboard-shellkanban
Use cases
dashboard
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
avatarbuttondropdown-menusidebartooltip
Files5 · source cached 10/8/2026, 6:16:25 AM
registry/blocks/sidebar/sidebar-2/source/app/sidebar/page.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
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
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
"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
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>
)
}