Sidebar
The Sidebar provides an application navigation shell that can collapse to an icon rail, slide fully off-canvas, or be toggled with a rail handle. It is implemented in both React and Svelte on top of Ark UI’s drawer and tooltip primitives, sharing the sidebarMenuButtonVariants cva and the --sidebar-* theme tokens from @cloudvoyant/helical-ui. Use the framework selector in the top-right to switch between implementations.
// apps/docs/src/components/examples/sidebar/icon/react.tsx
import {
SidebarProvider,
Sidebar,
SidebarHeader,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarMenu,
SidebarMenuLink,
SidebarRail,
SidebarInset,
SidebarTrigger,
Container,
} from '@cloudvoyant/helical-react';
import { Home, Inbox, Settings, LifeBuoy, PanelLeft } from 'lucide-react';
import { HelicalLogo } from '../../HelicalLogo';
const NAV = [
{ label: 'Home', icon: Home },
{ label: 'Inbox', icon: Inbox },
{ label: 'Settings', icon: Settings },
];
function AppSidebar() {
return (
<Sidebar collapsible="icon" className="h-full">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuLink
href="#"
size="lg"
icon={
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<HelicalLogo className="size-5" />
</div>
}
>
helical-ui
</SidebarMenuLink>
</SidebarMenu>
</SidebarHeader>
<SidebarContent className="scroll-fade scroll-fade-20 no-scrollbar">
<SidebarGroup label="Applications">
<SidebarMenu>
{NAV.map((item) => (
<SidebarMenuLink
key={item.label}
icon={<item.icon />}
tooltip={item.label}
isActive={item.label === 'Home'}
>
{item.label}
</SidebarMenuLink>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuLink icon={<LifeBuoy />} tooltip="Help">
Help
</SidebarMenuLink>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
}
export default function ReactSidebarIcon() {
return (
<div className="relative h-[560px] rounded-lg bg-background [transform:translateZ(0)]">
<SidebarProvider className="h-full min-h-0">
<AppSidebar />
<SidebarInset>
<header className="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger>
<PanelLeft />
</SidebarTrigger>
<span className="text-sm font-medium">Icon rail sidebar</span>
</header>
<Container className="flex-1 py-6">
<p className="text-sm text-muted-foreground">
Click the trigger (or the rail, or press Cmd/Ctrl+B) to collapse this sidebar to an icon rail.
</p>
</Container>
</SidebarInset>
</SidebarProvider>
</div>
);
}<!-- apps/docs/src/components/examples/sidebar/icon/svelte.svelte -->
<script lang="ts">
import {
SidebarProvider,
Sidebar,
SidebarHeader,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarMenu,
SidebarMenuLink,
SidebarRail,
SidebarInset,
SidebarTrigger,
Container,
} from '@cloudvoyant/helical-svelte';
import { House as Home, Inbox, Settings, LifeBuoy } from 'lucide-svelte';
import HelicalLogo from '../../HelicalLogo.svelte';
const NAV = [
{ label: 'Home', icon: Home },
{ label: 'Inbox', icon: Inbox },
{ label: 'Settings', icon: Settings },
];
</script>
<div class="relative h-[560px] rounded-lg bg-background [transform:translateZ(0)]">
<SidebarProvider class="h-full min-h-0">
<Sidebar collapsible="icon" class="h-full">
<SidebarHeader>
<SidebarMenu>
{#snippet logoIcon()}
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<HelicalLogo class="size-5" />
</div>
{/snippet}
<SidebarMenuLink href="#" size="lg" icon={logoIcon}>
helical-ui
</SidebarMenuLink>
</SidebarMenu>
</SidebarHeader>
<SidebarContent class="scroll-fade scroll-fade-20 no-scrollbar">
<SidebarGroup label="Applications">
<SidebarMenu>
{#each NAV as item (item.label)}
{#snippet itemIcon()}
<item.icon />
{/snippet}
<SidebarMenuLink icon={itemIcon} tooltip={item.label} isActive={item.label === 'Home'}>
{item.label}
</SidebarMenuLink>
{/each}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
{#snippet helpIcon()}
<LifeBuoy />
{/snippet}
<SidebarMenuLink icon={helpIcon} tooltip="Help">
Help
</SidebarMenuLink>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header class="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M15 3v18" /></svg
>
</SidebarTrigger>
<span class="text-sm font-medium">Icon rail sidebar</span>
</header>
<Container class="flex-1 py-6">
<p class="text-sm text-muted-foreground">
Click the trigger (or the rail, or press Cmd/Ctrl+B) to collapse this sidebar to an icon rail.
</p>
</Container>
</SidebarInset>
</SidebarProvider>
</div>Examples
Offcanvas
// apps/docs/src/components/examples/sidebar/offcanvas/react.tsx
import {
SidebarProvider,
Sidebar,
SidebarHeader,
SidebarContent,
SidebarGroup,
SidebarMenu,
SidebarMenuLink,
SidebarInset,
SidebarTrigger,
Container,
} from '@cloudvoyant/helical-react';
import { Home, Inbox, Settings, PanelLeft } from 'lucide-react';
import { HelicalLogo } from '../../HelicalLogo';
const NAV = [
{ label: 'Home', icon: Home },
{ label: 'Inbox', icon: Inbox },
{ label: 'Settings', icon: Settings },
];
export default function ReactSidebarOffcanvas() {
return (
<div className="relative h-[560px] rounded-lg bg-background [transform:translateZ(0)]">
<SidebarProvider className="h-full min-h-0">
<Sidebar collapsible="offcanvas" className="h-full">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuLink
href="#"
size="lg"
icon={
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<HelicalLogo className="size-5" />
</div>
}
>
helical-ui
</SidebarMenuLink>
</SidebarMenu>
</SidebarHeader>
<SidebarContent className="scroll-fade scroll-fade-20 no-scrollbar">
<SidebarGroup label="Applications">
<SidebarMenu>
{NAV.map((item) => (
<SidebarMenuLink key={item.label} icon={<item.icon />} isActive={item.label === 'Home'}>
{item.label}
</SidebarMenuLink>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarInset>
<header className="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger>
<PanelLeft />
</SidebarTrigger>
<span className="text-sm font-medium">Offcanvas sidebar</span>
</header>
<Container className="flex-1 py-6">
<p className="text-sm text-muted-foreground">
Click the trigger (or press Cmd/Ctrl+B) to completely hide this sidebar off-screen.
</p>
</Container>
</SidebarInset>
</SidebarProvider>
</div>
);
}<!-- apps/docs/src/components/examples/sidebar/offcanvas/svelte.svelte -->
<script lang="ts">
import {
SidebarProvider,
Sidebar,
SidebarHeader,
SidebarContent,
SidebarGroup,
SidebarMenu,
SidebarMenuLink,
SidebarInset,
SidebarTrigger,
Container,
} from '@cloudvoyant/helical-svelte';
import { House as Home, Inbox, Settings } from 'lucide-svelte';
import HelicalLogo from '../../HelicalLogo.svelte';
const NAV = [
{ label: 'Home', icon: Home },
{ label: 'Inbox', icon: Inbox },
{ label: 'Settings', icon: Settings },
];
</script>
<div class="relative h-[560px] rounded-lg bg-background [transform:translateZ(0)]">
<SidebarProvider class="h-full min-h-0">
<Sidebar collapsible="offcanvas" class="h-full">
<SidebarHeader>
<SidebarMenu>
{#snippet logoIcon()}
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<HelicalLogo class="size-5" />
</div>
{/snippet}
<SidebarMenuLink href="#" size="lg" icon={logoIcon}>
helical-ui
</SidebarMenuLink>
</SidebarMenu>
</SidebarHeader>
<SidebarContent class="scroll-fade scroll-fade-20 no-scrollbar">
<SidebarGroup label="Applications">
<SidebarMenu>
{#each NAV as item (item.label)}
{#snippet itemIcon()}
<item.icon />
{/snippet}
<SidebarMenuLink icon={itemIcon} isActive={item.label === 'Home'}>
{item.label}
</SidebarMenuLink>
{/each}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarInset>
<header class="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M15 3v18" /></svg
>
</SidebarTrigger>
<span class="text-sm font-medium">Offcanvas sidebar</span>
</header>
<Container class="flex-1 py-6">
<p class="text-sm text-muted-foreground">
Click the trigger (or press Cmd/Ctrl+B) to completely hide this sidebar off-screen.
</p>
</Container>
</SidebarInset>
</SidebarProvider>
</div>Rail
// apps/docs/src/components/examples/sidebar/rail/react.tsx
import {
SidebarProvider,
Sidebar,
SidebarHeader,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarMenu,
SidebarMenuLink,
SidebarRail,
SidebarInset,
SidebarTrigger,
Container,
} from '@cloudvoyant/helical-react';
import { Home, Inbox, Settings, LifeBuoy, PanelLeft } from 'lucide-react';
import { HelicalLogo } from '../../HelicalLogo';
const NAV = [
{ label: 'Home', icon: Home },
{ label: 'Inbox', icon: Inbox },
{ label: 'Settings', icon: Settings },
];
export default function ReactSidebarRail() {
return (
<div className="relative h-[560px] rounded-lg bg-background [transform:translateZ(0)]">
<SidebarProvider className="h-full min-h-0">
<Sidebar collapsible="offcanvas" className="h-full">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuLink
href="#"
size="lg"
icon={
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<HelicalLogo className="size-5" />
</div>
}
>
helical-ui
</SidebarMenuLink>
</SidebarMenu>
</SidebarHeader>
<SidebarContent className="scroll-fade scroll-fade-20 no-scrollbar">
<SidebarGroup label="Applications">
<SidebarMenu>
{NAV.map((item) => (
<SidebarMenuLink
key={item.label}
icon={<item.icon />}
tooltip={item.label}
isActive={item.label === 'Home'}
>
{item.label}
</SidebarMenuLink>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuLink icon={<LifeBuoy />} tooltip="Help">
Help
</SidebarMenuLink>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header className="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger>
<PanelLeft />
</SidebarTrigger>
<span className="text-sm font-medium">Rail sidebar</span>
</header>
<Container className="flex-1 py-6">
<p className="text-sm text-muted-foreground">
Hover the rail handle on the sidebar edge (or use the trigger / Cmd/Ctrl+B) to collapse and expand.
</p>
</Container>
</SidebarInset>
</SidebarProvider>
</div>
);
}<!-- apps/docs/src/components/examples/sidebar/rail/svelte.svelte -->
<script lang="ts">
import {
SidebarProvider,
Sidebar,
SidebarHeader,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarMenu,
SidebarMenuLink,
SidebarRail,
SidebarInset,
SidebarTrigger,
Container,
} from '@cloudvoyant/helical-svelte';
import { House as Home, Inbox, Settings, LifeBuoy } from 'lucide-svelte';
import HelicalLogo from '../../HelicalLogo.svelte';
const NAV = [
{ label: 'Home', icon: Home },
{ label: 'Inbox', icon: Inbox },
{ label: 'Settings', icon: Settings },
];
</script>
<div class="relative h-[560px] rounded-lg bg-background [transform:translateZ(0)]">
<SidebarProvider class="h-full min-h-0">
<Sidebar collapsible="offcanvas" class="h-full">
<SidebarHeader>
<SidebarMenu>
{#snippet logoIcon()}
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<HelicalLogo class="size-5" />
</div>
{/snippet}
<SidebarMenuLink href="#" size="lg" icon={logoIcon}>
helical-ui
</SidebarMenuLink>
</SidebarMenu>
</SidebarHeader>
<SidebarContent class="scroll-fade scroll-fade-20 no-scrollbar">
<SidebarGroup label="Applications">
<SidebarMenu>
{#each NAV as item (item.label)}
{#snippet itemIcon()}
<item.icon />
{/snippet}
<SidebarMenuLink icon={itemIcon} tooltip={item.label} isActive={item.label === 'Home'}>
{item.label}
</SidebarMenuLink>
{/each}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
{#snippet helpIcon()}
<LifeBuoy />
{/snippet}
<SidebarMenuLink icon={helpIcon} tooltip="Help">
Help
</SidebarMenuLink>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header class="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M15 3v18" /></svg
>
</SidebarTrigger>
<span class="text-sm font-medium">Rail sidebar</span>
</header>
<Container class="flex-1 py-6">
<p class="text-sm text-muted-foreground">
Hover the rail handle on the sidebar edge (or use the trigger / Cmd/Ctrl+B) to collapse and expand.
</p>
</Container>
</SidebarInset>
</SidebarProvider>
</div>API Reference
The sidebar is a composition of named parts; all structural parts accept standard element attributes plus className/class, merged via cn.
SidebarProvider
Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen |
boolean |
true |
Default open state. |
open |
boolean |
— | Controlled open state. |
onOpenChange |
(open: boolean) => void |
— | Fires when open state changes. |
Sidebar
Props
| Prop | Type | Default | Description |
|---|---|---|---|
side |
'left' | 'right' |
'left' |
Which side the sidebar sits on. |
variant |
'sidebar' | 'floating' | 'inset' |
'sidebar' |
Layout variant. |
collapsible |
'offcanvas' | 'icon' | 'none' |
'offcanvas' |
Collapse behavior: icon leaves a thin icon rail, offcanvas hides it entirely, none disables collapsing. |
SidebarGroup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string |
— | Optional group heading, rendered as the SidebarGroupLabel. |
SidebarMenuLink
The concise nav-item shorthand: renders SidebarMenuItem > SidebarMenuButton in one component.
Props
Accepts all SidebarMenuButton props plus:
| Prop | Type | Default | Description |
|---|---|---|---|
icon |
ReactNode / snippet |
— | Icon rendered before the label. |
href |
string |
— | When set, renders an <a> instead of a <button>. |
children |
string |
— | The item label. |
SidebarMenuButton
Props
Accepts all HTMLButtonProps plus className/class, and:
| Prop | Type | Default | Description |
|---|---|---|---|
asChild |
boolean / snippet |
false |
Render the button as a different element (e.g. an <a>). |
isActive |
boolean |
false |
Marks the item active. |
variant |
'default' | 'outline' |
'default' |
Menu button style. |
size |
'default' | 'sm' | 'lg' |
'default' |
Menu button size. |
tooltip |
string or Ark TooltipContent props |
— | Label shown as a tooltip when the sidebar is collapsed to an icon rail. |
useSidebar
Return
state ('expanded' | 'collapsed'), open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar.
Other parts
SidebarHeader, SidebarFooter, SidebarContent, SidebarGroupAction, SidebarMenu, SidebarMenuItem, SidebarMenuAction, SidebarMenuBadge, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton, SidebarTrigger, SidebarRail, SidebarInset, SidebarSeparator, SidebarInput — each accepts its native element props plus className/class. SidebarTrigger and SidebarRail are toggle buttons (aria-label="Toggle Sidebar"); SidebarRail is non-focusable by default (tabIndex={-1}). SidebarMenuLink covers the common nav-item case; use SidebarMenuItem + SidebarMenuButton + SidebarMenuAction/SidebarMenuBadge for composite items (badge, hover action, submenu).
Accessibility
The mobile sheet uses Ark’s drawer primitive, so focus trapping, Esc dismissal, and the dialog role come from Ark. Tooltips use Ark’s tooltip primitive. SidebarTrigger, SidebarRail, and SidebarMenuButton are native buttons with accessible labels; Cmd/Ctrl+B toggles the sidebar. Non-interactive parts stay non-focusable.
Theming
No theme extension is required — the sidebar uses the --sidebar-* tokens already shipped in @cloudvoyant/helical-ui’s theme.css (light and dark).