hexui
blocksidebar-3source: ok · hit
Sidebar-3
A sidebar designed for AI applications
npx shadcn add "https://hexui.sh/r/sidebar-3.json"free
Where it goes
sidebardashboard-shell
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
Files7 · source cached 10/8/2026, 2:12:25 PM
registry/blocks/sidebar/sidebar-3/source/app/sidebar/page.tsx
import { AppSidebar } from "../../components/app-sidebar"
import { AppHeader } from "../../components/app-header"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { MessageCircle } from "lucide-react"
// Customize the expanded sidebar width via the `--sidebar-width` CSS variable.
const SIDEBAR_PROVIDER_STYLE = {
"--sidebar-width": "17rem",
} as React.CSSProperties
export default function Page() {
return (
<SidebarProvider defaultOpen={false} className="min-h-0 flex-1" style={SIDEBAR_PROVIDER_STYLE}>
<AppSidebar />
<SidebarInset className="bg-background">
<AppHeader />
<main className="flex flex-1 flex-col items-center justify-center p-8">
<div className="flex max-w-2xl flex-col items-center text-center">
<div className="mb-6 flex size-14 items-center justify-center rounded-2xl bg-foreground/5">
<MessageCircle className="size-7 text-foreground/70" />
</div>
<h1 className="mb-3 text-3xl font-semibold tracking-tight text-foreground">
How can I help you today?
</h1>
<p className="mb-8 max-w-md text-muted-foreground">
Start a new conversation or select an existing chat from the
sidebar.
</p>
</div>
</main>
</SidebarInset>
</SidebarProvider>
)
}
registry/blocks/sidebar/sidebar-3/source/components/app-brand.tsx
import { Command } from "lucide-react";
type AppBrandProps = {
href?: string;
className?: string;
size?: number;
};
export function AppBrand({ href = "#", className, size = 20 }: AppBrandProps) {
return (
// Centralize the brand UI so header and sidebar stay visually consistent.
<a href={href} className="flex items-center justify-start gap-2 font-medium">
<Command className={className} size={size} />
</a>
);
}
registry/blocks/sidebar/sidebar-3/source/components/app-header.tsx
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { SidebarTrigger } from "@/components/ui/sidebar"
import { ChevronDown } from "lucide-react"
const MODEL_OPTIONS = [
{
value: "hex-1.0-pro",
label: "Hex 1.0 Pro",
description: "Best for deep reasoning",
},
{
value: "hex-1.0",
label: "Hex 1.0",
description: "Balanced for everyday conversations",
},
{
value: "hex-1.0-lite",
label: "Hex 1.0 Lite",
description: "Faster responses for lightweight tasks",
},
] as const
export function AppHeader() {
const [selectedModel, setSelectedModel] = React.useState<string>(MODEL_OPTIONS[0].value)
// Keep the displayed model metadata in sync with the selected value (fallback to the first).
const activeModel =
MODEL_OPTIONS.find((model) => model.value === selectedModel) ?? MODEL_OPTIONS[0]
return (
<header className="flex h-12 shrink-0 items-center gap-2 sm:gap-3 px-3 sm:px-4">
<SidebarTrigger className="shrink-0 sm:hidden" />
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type="button"
variant="ghost"
className="h-9 min-w-0 max-w-55 justify-start gap-2 px-2.5 sm:max-w-65"
/>
}
>
<span className="truncate text-base font-medium">{activeModel.label}</span>
<ChevronDown className="size-4 shrink-0 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-72">
<DropdownMenuRadioGroup
value={selectedModel}
onValueChange={setSelectedModel}
>
{MODEL_OPTIONS.map((model) => (
<DropdownMenuRadioItem
key={model.value}
value={model.value}
className="py-2"
>
<div className="flex min-w-0 flex-col gap-0.5 pr-4">
<span className="truncate font-medium text-foreground">
{model.label}
</span>
<span className="text-xs text-muted-foreground">
{model.description}
</span>
</div>
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</header>
)
}
registry/blocks/sidebar/sidebar-3/source/components/app-sidebar.tsx
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar"
import {
PanelLeftIcon,
Search,
SquarePen,
Telescope,
GalleryVerticalEnd,
X,
} from "lucide-react"
import { AppBrand } from "./app-brand"
import { NavHeader } from "./nav-header"
import { NavRecents } from "./nav-recents"
import { NavUser } from "./nav-user"
const data = {
user: {
name: "Rion",
email: "m@example.com",
avatar: "https://avatars.githubusercontent.com/u/114809507",
},
navMain: [
{
title: "New chat",
url: "#",
icon: SquarePen,
isActive: true,
},
{
title: "Search chat",
url: "#",
icon: Search,
},
{
title: "Projects",
url: "#",
icon: GalleryVerticalEnd,
},
{
title: "Deep research",
url: "#",
icon: Telescope,
},
],
}
const conversationItems = [
{ title: "Q2 growth strategy brainstorm", url: "#" },
{ title: "Landing page copy ideas", url: "#" },
{ title: "Summarize customer interview notes", url: "#" },
{ title: "Create a launch checklist", url: "#" },
{ title: "Refactor sidebar animation", url: "#" },
{ title: "Write onboarding email sequence", url: "#" },
{ title: "Design a pricing page structure", url: "#" },
{ title: "Research competitors in AI tooling", url: "#" },
{ title: "Generate social post variations", url: "#" },
]
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader className="gap-3">
<SidebarHeaderContent />
<NavHeader items={data.navMain} />
</SidebarHeader>
<SidebarContent className="px-2 py-3">
<NavRecents items={conversationItems} />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
)
}
// Renders the sidebar header per state: mobile drawer, collapsed (icon-only), and expanded.
// Each branch returns its own layout and controls.
function SidebarHeaderContent() {
const { isMobile, setOpenMobile, state, toggleSidebar } = useSidebar()
const isCollapsed = state === "collapsed"
if (isMobile) {
return (
<div className="flex h-8 items-center justify-between gap-2">
<AppBrand className="ml-1 size-5" />
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground"
onClick={() => setOpenMobile(false)}
aria-label="Close sidebar"
>
<X className="size-4" />
</Button>
</div>
)
}
if (isCollapsed) {
return (
<div className="flex h-8 items-center justify-start">
{/* On hover the brand fades out and the expand icon fades in (RTL-flipped). */}
<button
type="button"
className="group flex size-8 items-center justify-center transition-colors"
onClick={() => toggleSidebar()}
aria-label="Expand sidebar"
>
<div className="relative flex size-8 items-center justify-center">
<AppBrand className="transition-opacity group-hover:opacity-0" size={20} />
<PanelLeftIcon className="cn-rtl-flip absolute size-4 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
</div>
</button>
</div>
)
}
return (
<div className="flex h-8 items-center gap-2">
<div className="flex shrink-0 items-center gap-2">
<AppBrand className="ml-1 size-5" />
</div>
<div className="ml-auto flex items-center gap-1">
<Button
type="button"
variant="ghost"
size="icon-sm"
className="text-muted-foreground md:hidden"
onClick={() => toggleSidebar()}
aria-label="Collapse sidebar"
>
<X className="size-4" />
</Button>
<SidebarTrigger className="hidden text-muted-foreground md:flex" />
</div>
</div>
)
}
registry/blocks/sidebar/sidebar-3/source/components/nav-header.tsx
"use client"
import React from "react"
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"
export type NavigationItem = {
title: string
url: string
icon: React.ElementType
isActive?: boolean
}
type SidebarHeaderMenuProps = {
items: NavigationItem[]
}
function NavHeaderContent({ items }: SidebarHeaderMenuProps) {
return (
<SidebarMenu className="gap-0.5">
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton isActive={item.isActive} tooltip={item.title}>
<a className="flex items-center gap-2" href={item.url}>
<item.icon className="size-4 shrink-0" />
{/* Label fades out when the sidebar collapses to icon-only mode. */}
<span className="shrink-0 w-max whitespace-nowrap transition-opacity duration-200 ease-linear delay-100 group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:delay-0">
{item.title}
</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
)
}
// Memoized: the header menu is static, so we skip re-renders when the sidebar toggles.
export const NavHeader = React.memo(NavHeaderContent)
registry/blocks/sidebar/sidebar-3/source/components/nav-recents.tsx
import React from "react"
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
export type ConversationItem = {
title: string
url: string
isActive?: boolean
}
type NavRecentsProps = {
items: ConversationItem[]
}
function NavRecentsContent({ items }: NavRecentsProps) {
return (
// Whole group hides (and ignores pointer events) when the sidebar is icon-only.
<SidebarGroup className="p-0 transition-opacity duration-200 ease-linear delay-100 group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:delay-0">
<SidebarGroupLabel className="px-2 delay-100 group-data-[collapsible=icon]:delay-0 group-data-[collapsible=icon]:mt-0!">Recent</SidebarGroupLabel>
<SidebarGroupContent className="pt-1">
<SidebarMenu className="gap-0.5">
{items.map((item) => (
<SidebarMenuItem className="h-8" key={item.title}>
<SidebarMenuButton isActive={item.isActive} tooltip={item.title}>
<a className="flex items-center gap-2" href={item.url}>
<span className="shrink-0 w-max whitespace-nowrap">{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}
// Memoized: the conversation list is static, so we skip re-renders when the sidebar toggles.
export const NavRecents = React.memo(NavRecentsContent)
registry/blocks/sidebar/sidebar-3/source/components/nav-user.tsx
"use client"
import {
User2,
CreditCard,
LifeBuoy,
LogOut,
Shapes,
ArrowUpRight,
} from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const { isMobile } = useSidebar()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
size="lg"
className="relative w-full data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
/>
}
>
<Avatar className="h-8 w-8 shrink-0 rounded-full grayscale">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-full">RN</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col text-left text-sm leading-tight transition-opacity duration-200 ease-linear delay-100 group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:delay-0">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
// Open downward on mobile, to the right on desktop, to keep the menu on screen.
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">RN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate text-foreground font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Shapes />
Personalization
</DropdownMenuItem>
<DropdownMenuItem>
<User2 />
Profile
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Settings
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<LifeBuoy />
Help
<ArrowUpRight className="ml-auto" />
</DropdownMenuItem>
<DropdownMenuItem variant="destructive">
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}