Sidebar
Composable app-shell column with header, footer, groups, menus, and a rail toggle. Collapses with a 200ms width transition, falls back to a Sheet on mobile, and persists open state to a cookie via Sidebar.Provider.
Usage
Wrap a page in Sidebar.Provider to get the collapsible
app shell. The preview below uses collapsible="none" to keep the demo self-contained.
Dashboard
Content area — replace with your page routes. The real shell uses Sidebar.Inset as the main region.
<script lang="ts">
import * as Sidebar from '$lib/components/ui/sidebar';
</script>
<Sidebar.Provider>
<Sidebar.Root collapsible="icon">
<Sidebar.Header>Workspace</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Navigation</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive>Dashboard</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar.Root>
<Sidebar.Inset>
<!-- Page content -->
</Sidebar.Inset>
</Sidebar.Provider>Anatomy
- Sidebar.Provider — shell + open state + cookie persistence
- Sidebar.Root — the sidebar column (variant: sidebar | floating | inset)
- Sidebar.Header / Content / Footer — vertical slots
- Sidebar.Group + GroupLabel + GroupContent + GroupAction — section framing
- Sidebar.Menu + MenuItem + MenuButton + MenuAction + MenuBadge — primary nav items
- Sidebar.MenuSub + MenuSubItem + MenuSubButton — nested items
- Sidebar.MenuSkeleton — loading placeholder rows
- Sidebar.Rail — edge handle that toggles collapsed state
- Sidebar.Trigger — any-location toggle button
- Sidebar.Separator — divider between groups
- Sidebar.Input — search field styled for the rail
- Sidebar.Inset — the main content region paired with the sidebar
<script lang="ts">
import * as Sheet from '$lib/components/ui/sheet/index.js';
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
import { SIDEBAR_WIDTH_MOBILE } from './constants.js';
import { useSidebar } from './context.svelte.js';
let {
ref = $bindable(null),
side = 'left',
variant = 'sidebar',
collapsible = 'offcanvas',
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
side?: 'left' | 'right';
variant?: 'sidebar' | 'floating' | 'inset';
collapsible?: 'offcanvas' | 'icon' | 'none';
} = $props();
const sidebar = useSidebar();
</script>
{#if collapsible === 'none'}
<div
class={cn(
'flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground',
className
)}
bind:this={ref}
data-substrate="3"
{...restProps}
>
{@render children?.()}
</div>
{:else if sidebar.isMobile}
<Sheet.Root bind:open={() => sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} {...restProps}>
<Sheet.Content
bind:ref
data-sidebar="sidebar"
data-slot="sidebar"
data-substrate="3"
data-mobile="true"
class={cn(
'w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden',
className
)}
style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};"
{side}
>
<Sheet.Header class="sr-only">
<Sheet.Title>Sidebar</Sheet.Title>
<Sheet.Description>Displays the mobile sidebar.</Sheet.Description>
</Sheet.Header>
<div class="flex h-full w-full flex-col">
{@render children?.()}
</div>
</Sheet.Content>
</Sheet.Root>
{:else}
<div
bind:this={ref}
id="sidebar"
class="group peer hidden text-sidebar-foreground md:block"
data-state={sidebar.state}
data-collapsible={sidebar.state === 'collapsed' ? collapsible : ''}
data-variant={variant}
data-side={side}
data-slot="sidebar"
data-substrate="3"
>
<!-- This is what handles the sidebar gap on desktop -->
<div
data-slot="sidebar-gap"
class={cn(
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)'
)}
></div>
<div
data-slot="sidebar-container"
class={cn(
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
side === 'left'
? 'start-0 group-data-[collapsible=offcanvas]:start-[calc(var(--sidebar-width)*-1)]'
: 'end-0 group-data-[collapsible=offcanvas]:end-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-e group-data-[side=right]:border-s',
className
)}
{...restProps}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
class="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
>
{@render children?.()}
</div>
</div>
</div>
{/if}
App shell
Full composition: header + two nav groups + separator + user footer + inset main region. This
preview uses collapsible="none" so the sidebar stays
inside the preview box — the collapsible "icon" / "offcanvas" modes use position: fixed against the viewport and only work
at the root of a real app layout, not embedded inside the docs page.
Inset content area. In a real app this is wrapped with Sidebar.Inset and the shell lives at the route layout root so collapsible="icon" can position the sidebar fixed
to the viewport edge.
<script lang="ts">
import * as Sidebar from '$lib/components/ui/sidebar';
import { Icon } from '$lib/components/ui/icon';
</script>
<Sidebar.Provider>
<Sidebar.Root collapsible="icon">
<Sidebar.Header>
<div class="flex items-center gap-2 px-2 py-1">
<div class="size-6 rounded-md bg-primary"></div>
<span class="font-semibold text-sidebar-foreground group-data-collapsible=icon:hidden">Bloom</span>
</div>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Platform</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive tooltipContent="Dashboard">
<Icon name="layout-grid-line" aria-hidden="true" />
<span>Dashboard</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.MenuButton tooltipContent="Documents">
<Icon name="file-text-line" aria-hidden="true" />
<span>Documents</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Settings</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton tooltipContent="Team">
<Icon name="group-line" aria-hidden="true" />
<span>Team</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.MenuButton tooltipContent="Settings">
<Icon name="settings-line" aria-hidden="true" />
<span>Settings</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton tooltipContent="Account">
<Icon name="account-circle-line" aria-hidden="true" />
<span>Ada Bloom</span>
<Icon name="arrow-down-s-line" class="ml-auto" aria-hidden="true" />
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Footer>
<Sidebar.Rail />
</Sidebar.Root>
<Sidebar.Inset>
<header class="flex h-12 items-center border-b border-border px-4 gap-2">
<Sidebar.Trigger />
<span class="text-sm font-medium">Dashboard</span>
</header>
<main class="p-6">
<p class="text-sm text-muted-foreground">Page content goes here.</p>
</main>
</Sidebar.Inset>
</Sidebar.Provider>Nested menu
Sidebar.MenuSub tucks a child list under a top-level item
— common pattern for collapsible doc trees or nested settings.
Nested items live under Sidebar.MenuSub.
<Sidebar.MenuItem>
<Sidebar.MenuButton><Icon name="file-text-line" /><span>Documents</span></Sidebar.MenuButton>
<Sidebar.MenuSub>
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton href="#">Drafts</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
<Sidebar.MenuSubItem>
<Sidebar.MenuSubButton href="#" isActive>Published</Sidebar.MenuSubButton>
</Sidebar.MenuSubItem>
</Sidebar.MenuSub>
</Sidebar.MenuItem>Sidebar.Root props
Sidebar.Provider accepts open, defaultOpen, onOpenChange. Every subcomponent forwards HTML attributes unless noted.
| Prop | Type | Default | Description |
|---|---|---|---|
'left' | 'right' | 'left' | Which edge the sidebar docks to. | |
'sidebar' | 'floating' | 'inset' | 'sidebar' | Shell style. Floating detaches the column with a ring + shadow; inset pairs with a rounded inset main region. | |
'offcanvas' | 'icon' | 'none' | 'offcanvas' | How the sidebar collapses. Offcanvas slides the column off-screen; icon shrinks to a narrow rail; none disables collapsing. | |
string | — | Merged onto the container via tailwind-merge. |
Sidebar.MenuButton props
| Prop | Type | Default | Description |
|---|---|---|---|
boolean | false | Marks the button as the active route. Applies the active background + foreground. | |
'default' | 'outline' | 'default' | Visual treatment of the button. | |
'sm' | 'default' | 'lg' | 'default' | Controls height and padding. | |
string | Snippet | — | Text shown in a Tooltip when the sidebar is collapsed to icon mode. |