shadimage
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 · light
iconslucide-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>
    )
}

More from hexui

Browse all components in this library →