Components
Sidebar
A composable, collapsible navigation sidebar with built-in brand header and mobile support.
Import
import {
AppSidebarProvider,
AppSidebar,
AppSidebarBrand,
AppSidebarTrigger,
AppSidebarRail,
AppSidebarInset,
AppSidebarHeader,
AppSidebarContent,
AppSidebarFooter,
AppSidebarSeparator,
AppSidebarInput,
AppSidebarGroup,
AppSidebarGroupLabel,
AppSidebarGroupAction,
AppSidebarGroupContent,
AppSidebarMenu,
AppSidebarMenuItem,
AppSidebarMenuButton,
AppSidebarMenuAction,
AppSidebarMenuBadge,
AppSidebarMenuSkeleton,
AppSidebarMenuSub,
AppSidebarMenuSubItem,
AppSidebarMenuSubButton,
useSidebar,
} from "@/components/primitives/AppSidebar"API Reference
AppSidebarProvider
Wraps the entire layout. Manages open/collapsed state and provides context to all child sidebar components. Open state is persisted to a cookie automatically.
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultOpen | boolean | true | Initial open state (uncontrolled). |
| open | boolean | — | Controlled open state. |
| onOpenChange | (open: boolean) => void | — | Callback fired when open state changes. |
AppSidebar
| Prop | Type | Default | Description |
|---|---|---|---|
| side | "left" | "right" | "left" | Which edge the sidebar is anchored to. |
| variant | "sidebar" | "floating" | "inset" | "sidebar" | Visual presentation style. |
| collapsible | "offcanvas" | "icon" | "none" | "offcanvas" | offcanvas: slides off-screen. icon: collapses to icon width. none: always visible. |
AppSidebarBrand
Place as the first child of AppSidebarHeader. The logo is always horizontally centered. When a dropdown is provided, the chevron is absolutely positioned to the right edge so it does not shift the logo off-center. Accepts either a text string or a logo component for the logo prop.
| Prop | Type | Default | Description |
|---|---|---|---|
| logo | string | ReactNode | <WhiteLogo width={90} height={28} /> | Text label or logo component. Defaults to the white Viana Kit logo for use on dark backgrounds. |
| collapsedLogo | ReactNode | <WhiteSymbol width={24} height={24} /> | Icon shown in the header when the sidebar is collapsed to icon mode. |
| dropdown | AppSidebarBrandDropdownItem[] | — | Structured item list. When omitted the brand renders as a plain non-interactive element. |
| showChevron | boolean | true | Show the caret icon. Only applies when dropdown is provided. |
| className | string | — | Additional classes for the brand container. |
AppSidebarBrandDropdownItem
Two shapes are accepted in the same array — regular items and separators:
// Regular item
{ label: string; onClick?: () => void; icon?: ReactNode; disabled?: boolean }
// Separator — renders a visual divider
{ separator: true }AppSidebarMenuButton
No background fill in the default or hover state. The primary brand color background is applied only when isActive={true}. Do not add manual bg-* or hover:bg-* classes — fill behaviour is intentionally controlled by isActive only.
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | false | Render as child element (e.g. a router Link). |
| isActive | boolean | false | Marks this item as the active route. Applies primary brand color background + medium font weight. |
| variant | "default" | "outline" | "default" | Visual style. |
| size | "default" | "sm" | "lg" | "default" | Height and text size. |
| tooltip | string | TooltipContentProps | — | Tooltip shown when sidebar is collapsed to icon mode. |
Usage
Default logo
Omit the logo prop entirely to use the built-in white Viana Kit logo at 90×28, centered in the header.
<AppSidebarHeader>
<AppSidebarBrand />
</AppSidebarHeader>Text logo
<AppSidebarHeader>
<AppSidebarBrand logo="My App" />
</AppSidebarHeader>Image logo
Pass any ReactNode as the logo prop. 90×28 is the standard sidebar header size.
import { WhiteLogo } from "@/assets/logos"
<AppSidebarHeader>
<AppSidebarBrand logo={<WhiteLogo width={90} height={28} />} />
</AppSidebarHeader>With workspace dropdown
Pass a dropdown array to turn the brand into a dropdown trigger. The chevron is shown by default, positioned to the right so the logo stays centered. Use { separator: true } to insert a visual divider.
<AppSidebarBrand
dropdown={[
{ label: "Acme Corp", onClick: () => switchWorkspace("acme") },
{ label: "Personal", onClick: () => switchWorkspace("personal") },
{ separator: true },
{ label: "Create workspace", onClick: openCreateModal },
]}
/>Dropdown with icons
import { UserIcon, CreditCardIcon, LogOutIcon } from "lucide-react"
<AppSidebarBrand
dropdown={[
{ label: "Profile", icon: <UserIcon className="size-4" />, onClick: goToProfile },
{ label: "Billing", icon: <CreditCardIcon className="size-4" />, onClick: goToBilling },
{ separator: true },
{ label: "Log out", icon: <LogOutIcon className="size-4" />, onClick: logout },
]}
/>Disable the chevron
Set showChevron={false} to hide the caret while keeping the dropdown fully functional. Useful when the logo itself carries enough affordance, or for a minimal header style.
<AppSidebarBrand dropdown={items} showChevron={false} />Active state
Pass isActive to mark the current route. No background is shown for inactive or hovered items.
<AppSidebarMenuButton isActive>Dashboard</AppSidebarMenuButton>
<AppSidebarMenuButton>Projects</AppSidebarMenuButton>Menu button as link
<AppSidebarMenuButton asChild isActive>
<a href="/dashboard">Dashboard</a>
</AppSidebarMenuButton>Collapsible sidebar with trigger
Add AppSidebarRail for a drag-to-resize handle and AppSidebarTrigger for a toggle button. The keyboard shortcut ⌘B is wired automatically.
<AppSidebarProvider>
<AppSidebar collapsible="icon">
<AppSidebarHeader>
<AppSidebarBrand dropdown={workspaces} />
</AppSidebarHeader>
<AppSidebarContent>...</AppSidebarContent>
<AppSidebarRail />
</AppSidebar>
<AppSidebarInset>
<header className="flex items-center gap-2 border-b border-border px-4 py-3">
<AppSidebarTrigger />
</header>
<main>Page content</main>
</AppSidebarInset>
</AppSidebarProvider>Containing sidebar inside a preview box
When embedding a sidebar inside a fixed-height container, collapsible="icon" uses position: fixed which breaks out of the parent. Add [contain:layout] to the wrapper to create a containing block.
<div className="relative h-96 overflow-hidden rounded-lg border [contain:layout]">
<AppSidebarProvider className="min-h-0 h-full">
<AppSidebar collapsible="icon">...</AppSidebar>
<AppSidebarInset>...</AppSidebarInset>
</AppSidebarProvider>
</div>Source
"use client"
import * as React from "react"
import { ChevronDown } from "lucide-react"
import { cn } from "../../lib/utils"
import { WhiteLogo, WhiteSymbol } from "../../assets/logos"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu"
import { Sidebar, SidebarProvider, useSidebar, /* ... */ } from "../ui/sidebar"
export type AppSidebarBrandDropdownItem =
| { separator: true }
| { label: string; onClick?: () => void; icon?: React.ReactNode; disabled?: boolean }
function AppSidebarBrand({
logo = <WhiteLogo width={90} height={28} />,
collapsedLogo = <WhiteSymbol width={24} height={24} />,
dropdown,
showChevron = true,
className,
}) {
const { state } = useSidebar()
const isCollapsed = state === "collapsed"
const inner = (
<div className="relative flex w-full items-center justify-center py-1">
{isCollapsed
? collapsedLogo
: typeof logo === "string"
? <span className="text-sm font-semibold text-foreground">{logo}</span>
: logo}
{!isCollapsed && dropdown && showChevron && (
<ChevronDown className="absolute right-0 size-4 shrink-0 text-muted-foreground" />
)}
</div>
)
if (!dropdown || isCollapsed) {
return <div className={cn("flex items-center px-2", className)}>{inner}</div>
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className={cn("flex w-full items-center rounded-md px-2 outline-none hover:bg-sidebar-accent ...", className)}>
{inner}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="start" className="min-w-(--radix-dropdown-menu-trigger-width)">
{dropdown.map((item, i) =>
"separator" in item && item.separator
? <DropdownMenuSeparator key={i} />
: <DropdownMenuItem key={i} disabled={item.disabled} onClick={item.onClick}>
{item.icon}{item.label}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)
}
function AppSidebarMenuButton({ className, ...props }) {
return (
<SidebarMenuButton
className={cn(
// Remove fill on hover and CSS :active
"hover:bg-transparent hover:text-sidebar-foreground",
"active:bg-transparent active:text-sidebar-foreground",
// data-active: matches ALL buttons in Tailwind v4 (attribute always present) — reset
"data-active:bg-transparent data-active:font-normal data-active:text-sidebar-foreground",
// Re-apply primary brand color only for isActive={true}
"data-[active=true]:bg-primary data-[active=true]:font-medium data-[active=true]:text-primary-foreground",
className
)}
{...props}
/>
)
}
// All other App* wrappers are passthrough — see full source in the repo
export { AppSidebar, AppSidebarBrand, AppSidebarMenuButton, AppSidebarProvider, /* ... */ }