Sidebar

An app-shell navigation column. It collapses out of view or down to an icon rail on desktop, and becomes a drawer on small screens.

Inbox

Collapse the panel to an icon rail with the button in this header.

Press the + beside a page to nest a new one, or search to jump anywhere in Acme.

<script lang="ts">
    import Bell from '@lucide/svelte/icons/bell';
    import ChevronDown from '@lucide/svelte/icons/chevron-down';
    import CreditCard from '@lucide/svelte/icons/credit-card';
    import FileText from '@lucide/svelte/icons/file-text';
    import House from '@lucide/svelte/icons/house';
    import Inbox from '@lucide/svelte/icons/inbox';
    import LogOut from '@lucide/svelte/icons/log-out';
    import Plus from '@lucide/svelte/icons/plus';
    import Search from '@lucide/svelte/icons/search';
    import Settings from '@lucide/svelte/icons/settings';
    import SquarePen from '@lucide/svelte/icons/square-pen';
    import Trash from '@lucide/svelte/icons/trash-2';
    import UserPlus from '@lucide/svelte/icons/user-plus';
    import UserRound from '@lucide/svelte/icons/user-round';
    import { Button } from '@sivir-ui/svelte/components/button';
    import * as Command from '@sivir-ui/svelte/components/command';
    import * as DropdownMenu from '@sivir-ui/svelte/components/dropdown-menu';
    import { Input } from '@sivir-ui/svelte/components/input';
    import * as Modal from '@sivir-ui/svelte/components/modal';
    import Shortcut from '@sivir-ui/svelte/components/shortcut';
    import * as Sidebar from '@sivir-ui/svelte/components/sidebar';
    import { Switch } from '@sivir-ui/svelte/components/switch';
    import { cn } from '@sivir-ui/svelte/utils';

    type Page = {
        id: number;
        title: string;
        children: Page[];
    };

    let nextId = 5;
    let pages = $state<Page[]>([
        { id: 1, title: 'Roadmap', children: [] },
        { id: 2, title: 'Weekly planning', children: [] },
        { id: 3, title: 'Design reviews', children: [] },
        { id: 4, title: 'Hiring loop', children: [] }
    ]);
    let trashed = $state<string[]>([]);
    let open = $state(true);
    let active = $state('Inbox');

    let workspace = $state('Acme');
    let commandOpen = $state(false);
    let createOpen = $state(false);
    let settingsOpen = $state(false);
    let inviteOpen = $state(false);
    let newTitle = $state('');
    let parentId = $state<number | null>(null);
    let inviteEmail = $state('');
    let displayName = $state('Aidan');
    let notifications = $state(true);

    const parentTitle = $derived(parentId === null ? undefined : findPage(pages, parentId)?.title);

    function findPage(list: Page[], id: number): Page | undefined {
        for (const page of list) {
            if (page.id === id) {
                return page;
            }

            const child = findPage(page.children, id);

            if (child) {
                return child;
            }
        }

        return undefined;
    }

    function flatten(list: Page[]): Page[] {
        return list.flatMap((page) => [page, ...flatten(page.children)]);
    }

    function removePage(list: Page[], title: string): Page[] {
        return list
            .filter((page) => page.title !== title)
            .map((page) => ({ ...page, children: removePage(page.children, title) }));
    }

    function openCreate(parent: number | null) {
        parentId = parent;
        newTitle = '';
        createOpen = true;
    }

    function createPage() {
        const title = newTitle.trim();

        if (!title) {
            return;
        }

        const page: Page = { id: nextId++, title, children: [] };

        if (parentId === null) {
            pages.push(page);
        } else {
            findPage(pages, parentId)?.children.push(page);
        }

        active = title;
    }

    function trashActivePage() {
        const isPage = flatten(pages).some((page) => page.title === active);

        if (!isPage) {
            return;
        }

        trashed.push(active);
        pages = removePage(pages, active);
        active = 'Home';
    }

    function inviteTeammate() {
        inviteEmail = '';
    }
</script>

{#snippet pageItem(page: Page)}
    <Sidebar.Item>
        <Sidebar.ItemButton
            active={active === page.title}
            tooltip={page.title}
            onclick={() => (active = page.title)}
        >
            <FileText aria-hidden="true" />
            <Sidebar.ItemLabel>{page.title}</Sidebar.ItemLabel>
        </Sidebar.ItemButton>
        <Sidebar.ItemAction
            aria-label={`Add a page inside ${page.title}`}
            onclick={() => openCreate(page.id)}
        >
            <Plus aria-hidden="true" />
        </Sidebar.ItemAction>
        {#if page.children.length > 0}
            <Sidebar.Menu>
                {#each page.children as child (child.id)}
                    {@render pageItem(child)}
                {/each}
            </Sidebar.Menu>
        {/if}
    </Sidebar.Item>
{/snippet}

<Sidebar.Root
    bind:open
    collapsible="icon"
    variant="inset"
    class="h-120 rounded-[var(--radius-xl)] border border-border"
>
    <Sidebar.Panel aria-label="Workspace">
        <Sidebar.Header
            class={cn(!open && 'gap-0', 'flex-row items-center justify-between transition-[gap] [transition-duration:var(--motion-duration-sheet)] ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none')}
        >
            <DropdownMenu.Root>
                <DropdownMenu.Trigger variant="ghost" class="min-w-0 justify-start gap-2 px-2">
                    <span
                        class="flex size-5 shrink-0 items-center justify-center rounded-[var(--radius-sm)] bg-foreground text-xs text-background"
                        aria-hidden="true"
                        >{workspace.charAt(0)}</span
                    >
                    <Sidebar.ItemLabel>{workspace}</Sidebar.ItemLabel>
                    <ChevronDown
                        aria-hidden="true"
                        class={cn(!open && 'w-0 opacity-0', 'size-3.5 shrink-0 text-foreground-muted transition-[width,opacity] [transition-duration:var(--motion-duration-sheet)] ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none')}
                    />
                </DropdownMenu.Trigger>
                <DropdownMenu.Content class="min-w-[14rem]">
                    <DropdownMenu.Label>Workspaces</DropdownMenu.Label>
                    {#each ['Acme', 'Globex', 'Initech'] as name (name)}
                        <DropdownMenu.Item callback={() => (workspace = name)}>
                            <span class="flex items-center gap-2">
                                <span
                                    class="flex size-4 items-center justify-center rounded-[var(--radius-sm)] bg-foreground text-[10px] text-background"
                                    aria-hidden="true"
                                    >{name.charAt(0)}</span
                                >
                                {name}
                            </span>
                        </DropdownMenu.Item>
                    {/each}
                    <DropdownMenu.Label>Account</DropdownMenu.Label>
                    <DropdownMenu.Item callback={() => (inviteOpen = true)}>
                        <span class="flex items-center gap-2"
                            ><UserPlus size={13} />
                            Invite teammates</span
                        >
                    </DropdownMenu.Item>
                    <DropdownMenu.Item callback={() => (settingsOpen = true)}>
                        <span class="flex items-center gap-2"
                            ><Settings size={13} />
                            Preferences</span
                        >
                    </DropdownMenu.Item>
                    <DropdownMenu.Item>
                        <span class="flex items-center gap-2"
                            ><CreditCard size={13} />
                            Billing</span
                        >
                    </DropdownMenu.Item>
                    <DropdownMenu.Item>
                        <span class="flex items-center gap-2"><LogOut size={13} /> Log out</span>
                    </DropdownMenu.Item>
                </DropdownMenu.Content>
            </DropdownMenu.Root>
            <Button
                variant="ghost"
                size="icon"
                aria-label="New page"
                class={cn(!open && 'pointer-events-none w-0 min-w-0 overflow-hidden px-0 opacity-0', 'shrink-0 transition-[width,opacity,padding] [transition-duration:var(--motion-duration-sheet)] ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none')}
                onclick={() => openCreate(null)}
            >
                <SquarePen aria-hidden="true" />
            </Button>
        </Sidebar.Header>
        <Sidebar.Content>
            <Sidebar.Group>
                <Sidebar.GroupContent>
                    <Sidebar.Menu>
                        <Sidebar.Item>
                            <Sidebar.ItemButton
                                tooltip="Search"
                                onclick={() => (commandOpen = true)}
                            >
                                <Search aria-hidden="true" />
                                <Sidebar.ItemLabel>Search</Sidebar.ItemLabel>
                            </Sidebar.ItemButton>
                        </Sidebar.Item>
                        <Sidebar.Item>
                            <Sidebar.ItemButton
                                active={active === 'Home'}
                                tooltip="Home"
                                onclick={() => (active = 'Home')}
                            >
                                <House aria-hidden="true" />
                                <Sidebar.ItemLabel>Home</Sidebar.ItemLabel>
                            </Sidebar.ItemButton>
                        </Sidebar.Item>
                        <Sidebar.Item>
                            <Sidebar.ItemButton
                                active={active === 'Inbox'}
                                tooltip="Inbox"
                                onclick={() => (active = 'Inbox')}
                            >
                                <Inbox aria-hidden="true" />
                                <Sidebar.ItemLabel>Inbox</Sidebar.ItemLabel>
                            </Sidebar.ItemButton>
                            <Sidebar.ItemBadge>3</Sidebar.ItemBadge>
                            <Sidebar.Menu>
                                <Sidebar.Item>
                                    <Sidebar.ItemButton
                                        active={active === 'Assigned'}
                                        tooltip="Assigned"
                                        onclick={() => (active = 'Assigned')}
                                    >
                                        <UserRound aria-hidden="true" />
                                        <Sidebar.ItemLabel>Assigned to me</Sidebar.ItemLabel>
                                    </Sidebar.ItemButton>
                                    <Sidebar.ItemBadge>1</Sidebar.ItemBadge>
                                </Sidebar.Item>
                                <Sidebar.Item>
                                    <Sidebar.ItemButton
                                        active={active === 'Subscribed'}
                                        tooltip="Subscribed"
                                        onclick={() => (active = 'Subscribed')}
                                    >
                                        <Bell aria-hidden="true" />
                                        <Sidebar.ItemLabel>Subscribed</Sidebar.ItemLabel>
                                    </Sidebar.ItemButton>
                                    <Sidebar.ItemBadge>2</Sidebar.ItemBadge>
                                </Sidebar.Item>
                            </Sidebar.Menu>
                        </Sidebar.Item>
                    </Sidebar.Menu>
                </Sidebar.GroupContent>
            </Sidebar.Group>
            <Sidebar.Group collapsible>
                <Sidebar.GroupLabel>Private</Sidebar.GroupLabel>
                <Sidebar.GroupContent>
                    <Sidebar.Menu>
                        {#each pages as page (page.id)}
                            {@render pageItem(page)}
                        {/each}
                    </Sidebar.Menu>
                </Sidebar.GroupContent>
            </Sidebar.Group>
        </Sidebar.Content>
        <Sidebar.Footer>
            <Sidebar.Menu>
                <Sidebar.Item>
                    <Sidebar.ItemButton tooltip="Settings" onclick={() => (settingsOpen = true)}>
                        <Settings aria-hidden="true" />
                        <Sidebar.ItemLabel>Settings</Sidebar.ItemLabel>
                    </Sidebar.ItemButton>
                </Sidebar.Item>
                <Sidebar.Item>
                    <Sidebar.ItemButton
                        active={active === 'Trash'}
                        tooltip="Trash"
                        onclick={() => (active = 'Trash')}
                    >
                        <Trash aria-hidden="true" />
                        <Sidebar.ItemLabel>Trash</Sidebar.ItemLabel>
                    </Sidebar.ItemButton>
                    {#if trashed.length > 0}
                        <Sidebar.ItemBadge>{trashed.length}</Sidebar.ItemBadge>
                    {/if}
                </Sidebar.Item>
            </Sidebar.Menu>
        </Sidebar.Footer>
    </Sidebar.Panel>
    <Sidebar.Inset>
        <header class="flex h-12 shrink-0 items-center gap-2 border-b border-border px-3">
            <Sidebar.Trigger />
            <span class="[font-weight:var(--font-weight-header)]">{active}</span>
        </header>
        <div class="flex flex-col gap-2 p-5 text-foreground-muted">
            {#if active === 'Trash'}
                {#if trashed.length === 0}
                    <p>Nothing in the trash.</p>
                {:else}
                    <p>Moved to trash: {trashed.join(', ')}.</p>
                {/if}
            {:else}
                <p>Collapse the panel to an icon rail with the button in this header.</p>
                <p>
                    Press the + beside a page to nest a new one, or search to jump anywhere in
                    {workspace}.
                </p>
            {/if}
        </div>
    </Sidebar.Inset>
</Sidebar.Root>

<Command.Root bind:open={commandOpen}>
    <Command.Content>
        <Command.Search placeholder="Search pages or run a commandโ€ฆ" />
        <Command.Results>
            <Command.Group heading="Jump to">
                <Command.Item name="Home" callback={() => (active = 'Home')}>
                    <House size={16} />
                    Home
                </Command.Item>
                <Command.Item name="Inbox" callback={() => (active = 'Inbox')}>
                    <Inbox size={16} />
                    Inbox
                </Command.Item>
                {#each flatten(pages) as page (page.id)}
                    <Command.Item name={page.title} callback={() => (active = page.title)}>
                        <FileText size={16} />
                        {page.title}
                    </Command.Item>
                {/each}
            </Command.Group>
            <Command.Group heading="Actions">
                <Command.Item name="New page" callback={() => openCreate(null)}>
                    <SquarePen size={16} />
                    <span class="flex-1">New page</span>
                    <Shortcut shortcut="N" />
                </Command.Item>
                <Command.Item name="Move page to trash" callback={trashActivePage}>
                    <Trash size={16} />
                    Move page to trash
                </Command.Item>
                <Command.Item name="Invite teammates" callback={() => (inviteOpen = true)}>
                    <UserPlus size={16} />
                    Invite teammates
                </Command.Item>
                <Command.Item name="Settings" callback={() => (settingsOpen = true)}>
                    <Settings size={16} />
                    Settings
                </Command.Item>
            </Command.Group>
        </Command.Results>
    </Command.Content>
</Command.Root>

<Modal.Root bind:open={createOpen} orientation="vertical">
    <Modal.Content>
        <Modal.Header>
            <Modal.Title>New page</Modal.Title>
            <Modal.Description>
                {parentTitle ? `Create a page inside ${parentTitle}.` : `Add a page to ${workspace}.`}
            </Modal.Description>
        </Modal.Header>
        <Modal.Body class="gap-4">
            <Input bind:value={newTitle} label="Title" placeholder="Untitled" />
        </Modal.Body>
        <Modal.Footer>
            <Modal.Close>
                Cancel
                <Shortcut shortcut="esc" />
            </Modal.Close>
            <Modal.Confirm onclick={createPage} disabled={newTitle.trim() === ''}>
                Create
                <Shortcut shortcut="enter" />
            </Modal.Confirm>
        </Modal.Footer>
    </Modal.Content>
</Modal.Root>

<Modal.Root bind:open={settingsOpen} orientation="vertical">
    <Modal.Content>
        <Modal.Header>
            <Modal.Title>Settings</Modal.Title>
            <Modal.Description>Manage how you appear and what you hear about.</Modal.Description>
        </Modal.Header>
        <Modal.Body class="gap-4">
            <Input bind:value={displayName} label="Display name" />
            <Switch
                bind:checked={notifications}
                label="Email notifications"
                description="Get a digest when someone mentions you."
            />
        </Modal.Body>
        <Modal.Footer>
            <Modal.Close>Close</Modal.Close>
        </Modal.Footer>
    </Modal.Content>
</Modal.Root>

<Modal.Root bind:open={inviteOpen} orientation="vertical">
    <Modal.Content>
        <Modal.Header>
            <Modal.Title>Invite teammates</Modal.Title>
            <Modal.Description
                >They'll get an email with a link to join {workspace}.</Modal.Description
            >
        </Modal.Header>
        <Modal.Body class="gap-4">
            <Input bind:value={inviteEmail} label="Email" placeholder="teammate@example.com" />
        </Modal.Body>
        <Modal.Footer>
            <Modal.Close>Cancel</Modal.Close>
            <Modal.Confirm onclick={inviteTeammate} disabled={inviteEmail.trim() === ''}>
                Send invite
            </Modal.Confirm>
        </Modal.Footer>
    </Modal.Content>
</Modal.Root>

Installation

Copy the command below and run it in your terminal.

bunx @sivir-ui/svelte add sidebar

Usage

Sidebar.Root lays out Sidebar.Panel and Sidebar.Inset in a row. It does not set its own height, so give it one, such as h-svh for a full-page shell. Bind open to keep or persist the desktop state, and openMobile for the drawer.

Set variant="inset" to raise the main area onto its own rounded surface. Set collapsible to offcanvas (the default) to slide the panel out of view, icon to shrink it to an icon rail, or none to keep it open. Below 768px the panel always becomes a drawer that Sidebar.Trigger opens, and following a link inside it closes it.

import * as Sidebar from '@sivir-ui/svelte/components/sidebar';

let open = $state(true);

<Sidebar.Root bind:open variant="inset" collapsible="icon" class="h-svh">
  <Sidebar.Panel>
    <Sidebar.Header><!-- workspace switcher --></Sidebar.Header>
    <Sidebar.Content>
      <Sidebar.Group collapsible>
        <Sidebar.GroupLabel>Private</Sidebar.GroupLabel>
        <Sidebar.GroupContent>
          <Sidebar.Menu>
            <Sidebar.Item>
              <Sidebar.ItemButton href="/inbox" active tooltip="Inbox">
                <InboxIcon />
                <Sidebar.ItemLabel>Inbox</Sidebar.ItemLabel>
              </Sidebar.ItemButton>
              <Sidebar.ItemBadge>3</Sidebar.ItemBadge>
            </Sidebar.Item>
          </Sidebar.Menu>
        </Sidebar.GroupContent>
      </Sidebar.Group>
    </Sidebar.Content>
    <Sidebar.Footer><!-- settings --></Sidebar.Footer>
  </Sidebar.Panel>
  <Sidebar.Inset>
    <header><Sidebar.Trigger /></header>
    <!-- page content -->
  </Sidebar.Inset>
</Sidebar.Root>

Examples

Default

The default variant separates the panel from the page with a border. The trigger slides the panel out of view. Hover and keyboard focus share one highlight that travels between rows across groups, the same one menus use.

Overview
<script lang="ts">
    import ChartLine from '@lucide/svelte/icons/chart-line';
    import FolderKanban from '@lucide/svelte/icons/folder-kanban';
    import House from '@lucide/svelte/icons/house';
    import Settings from '@lucide/svelte/icons/settings';
    import * as Sidebar from '@sivir-ui/svelte/components/sidebar';

    const items = [
        { label: 'Overview', icon: House },
        { label: 'Projects', icon: FolderKanban },
        { label: 'Analytics', icon: ChartLine },
        { label: 'Settings', icon: Settings }
    ];
    let active = $state('Overview');
</script>

<Sidebar.Root class="h-96 rounded-[var(--radius-xl)] border border-border">
    <Sidebar.Panel>
        <Sidebar.Header>
            <span class="px-2 pt-1 [font-weight:var(--font-weight-header)]">Acme</span>
        </Sidebar.Header>
        <Sidebar.Content>
            <Sidebar.Group>
                <Sidebar.GroupContent>
                    <Sidebar.Menu>
                        {#each items as item (item.label)}
                            <Sidebar.Item>
                                <Sidebar.ItemButton
                                    active={active === item.label}
                                    onclick={() => (active = item.label)}
                                >
                                    <item.icon aria-hidden="true" />
                                    <Sidebar.ItemLabel>{item.label}</Sidebar.ItemLabel>
                                </Sidebar.ItemButton>
                            </Sidebar.Item>
                        {/each}
                    </Sidebar.Menu>
                </Sidebar.GroupContent>
            </Sidebar.Group>
        </Sidebar.Content>
    </Sidebar.Panel>
    <Sidebar.Inset>
        <header class="flex h-12 shrink-0 items-center gap-2 border-b border-border px-3">
            <Sidebar.Trigger />
            <span class="[font-weight:var(--font-weight-header)]">{active}</span>
        </header>
    </Sidebar.Inset>
</Sidebar.Root>

Icon rail

With collapsible="icon", the collapsed panel keeps its icons. Labels stay available to screen readers, and the tooltip on each Sidebar.ItemButton shows them on hover. Group labels, badges, actions, and sub-menus hide, and collapsible groups stay open.

Home
<script lang="ts">
    import ChartLine from '@lucide/svelte/icons/chart-line';
    import FolderKanban from '@lucide/svelte/icons/folder-kanban';
    import House from '@lucide/svelte/icons/house';
    import Inbox from '@lucide/svelte/icons/inbox';
    import Settings from '@lucide/svelte/icons/settings';
    import * as Sidebar from '@sivir-ui/svelte/components/sidebar';

    type RailItem = {
        label: string;
        icon: typeof House;
        count?: number;
    };

    const items: RailItem[] = [
        { label: 'Home', icon: House },
        { label: 'Inbox', icon: Inbox, count: 12 },
        { label: 'Projects', icon: FolderKanban },
        { label: 'Analytics', icon: ChartLine }
    ];
    let open = $state(false);
    let active = $state('Home');
</script>

<Sidebar.Root
    bind:open
    collapsible="icon"
    variant="inset"
    class="h-96 rounded-[var(--radius-xl)] border border-border"
>
    <Sidebar.Panel>
        <Sidebar.Content>
            <Sidebar.Group>
                <Sidebar.GroupLabel>Workspace</Sidebar.GroupLabel>
                <Sidebar.GroupContent>
                    <Sidebar.Menu>
                        {#each items as item (item.label)}
                            <Sidebar.Item>
                                <Sidebar.ItemButton
                                    active={active === item.label}
                                    tooltip={item.label}
                                    onclick={() => (active = item.label)}
                                >
                                    <item.icon aria-hidden="true" />
                                    <Sidebar.ItemLabel>{item.label}</Sidebar.ItemLabel>
                                </Sidebar.ItemButton>
                                {#if item.count}
                                    <Sidebar.ItemBadge>{item.count}</Sidebar.ItemBadge>
                                {/if}
                            </Sidebar.Item>
                        {/each}
                    </Sidebar.Menu>
                </Sidebar.GroupContent>
            </Sidebar.Group>
        </Sidebar.Content>
        <Sidebar.Footer>
            <Sidebar.Menu>
                <Sidebar.Item>
                    <Sidebar.ItemButton tooltip="Settings">
                        <Settings aria-hidden="true" />
                        <Sidebar.ItemLabel>Settings</Sidebar.ItemLabel>
                    </Sidebar.ItemButton>
                </Sidebar.Item>
            </Sidebar.Menu>
        </Sidebar.Footer>
    </Sidebar.Panel>
    <Sidebar.Inset>
        <header class="flex h-12 shrink-0 items-center gap-2 border-b border-border px-3">
            <Sidebar.Trigger />
            <span class="[font-weight:var(--font-weight-header)]">{active}</span>
        </header>
    </Sidebar.Inset>
</Sidebar.Root>

Collapsible groups

Add collapsible to Sidebar.Group to turn its label into a toggle. Bind open to control it.

Shared is collapsed.

<script lang="ts">
    import FileText from '@lucide/svelte/icons/file-text';
    import * as Sidebar from '@sivir-ui/svelte/components/sidebar';

    let sharedOpen = $state(false);
</script>

<Sidebar.Root collapsible="none" class="h-96 rounded-[var(--radius-xl)] border border-border">
    <Sidebar.Panel>
        <Sidebar.Content>
            <Sidebar.Group collapsible>
                <Sidebar.GroupLabel>Private</Sidebar.GroupLabel>
                <Sidebar.GroupContent>
                    {@render pageList(['Reading list', 'Habit tracker', 'Weekly to-do list'])}
                </Sidebar.GroupContent>
            </Sidebar.Group>
            <Sidebar.Group collapsible bind:open={sharedOpen}>
                <Sidebar.GroupLabel>Shared</Sidebar.GroupLabel>
                <Sidebar.GroupContent>
                    {@render pageList(['Team wiki', 'Launch checklist'])}
                </Sidebar.GroupContent>
            </Sidebar.Group>
        </Sidebar.Content>
    </Sidebar.Panel>
    <Sidebar.Inset>
        <p class="p-5 text-foreground-muted">Shared is {sharedOpen ? 'expanded' : 'collapsed'}.</p>
    </Sidebar.Inset>
</Sidebar.Root>

{#snippet pageList(pages: string[])}
    <Sidebar.Menu>
        {#each pages as page (page)}
            <Sidebar.Item>
                <Sidebar.ItemButton>
                    <FileText aria-hidden="true" />
                    <Sidebar.ItemLabel>{page}</Sidebar.ItemLabel>
                </Sidebar.ItemButton>
            </Sidebar.Item>
        {/each}
    </Sidebar.Menu>
{/snippet}

Badges and actions

Sidebar.ItemBadge shows a trailing count. Sidebar.ItemAction appears on hover or focus and takes the badge's place. It needs an aria-label. On touch screens it is always visible.

Hover a folder to reveal its options.

<script lang="ts">
    import Archive from '@lucide/svelte/icons/archive';
    import Ellipsis from '@lucide/svelte/icons/ellipsis';
    import FilePen from '@lucide/svelte/icons/file-pen';
    import Inbox from '@lucide/svelte/icons/inbox';
    import Send from '@lucide/svelte/icons/send';
    import * as Sidebar from '@sivir-ui/svelte/components/sidebar';

    const folders = [
        { label: 'Inbox', icon: Inbox, count: 12 },
        { label: 'Drafts', icon: FilePen, count: 2 },
        { label: 'Sent', icon: Send, count: 0 },
        { label: 'Archive', icon: Archive, count: 0 }
    ];
    let active = $state('Inbox');
</script>

<Sidebar.Root collapsible="none" class="h-80 rounded-[var(--radius-xl)] border border-border">
    <Sidebar.Panel>
        <Sidebar.Content>
            <Sidebar.Group>
                <Sidebar.GroupLabel>Mail</Sidebar.GroupLabel>
                <Sidebar.GroupContent>
                    <Sidebar.Menu>
                        {#each folders as folder (folder.label)}
                            <Sidebar.Item>
                                <Sidebar.ItemButton
                                    active={active === folder.label}
                                    onclick={() => (active = folder.label)}
                                >
                                    <folder.icon aria-hidden="true" />
                                    <Sidebar.ItemLabel>{folder.label}</Sidebar.ItemLabel>
                                </Sidebar.ItemButton>
                                {#if folder.count > 0}
                                    <Sidebar.ItemBadge>{folder.count}</Sidebar.ItemBadge>
                                {/if}
                                <Sidebar.ItemAction aria-label={`${folder.label} options`}>
                                    <Ellipsis aria-hidden="true" />
                                </Sidebar.ItemAction>
                            </Sidebar.Item>
                        {/each}
                    </Sidebar.Menu>
                </Sidebar.GroupContent>
            </Sidebar.Group>
        </Sidebar.Content>
    </Sidebar.Panel>
    <Sidebar.Inset>
        <p class="p-5 text-foreground-muted">Hover a folder to reveal its options.</p>
    </Sidebar.Inset>
</Sidebar.Root>

Nested items

Place a Sidebar.Menu inside a Sidebar.Item to indent it under that item. Pass href to render a link, and active to mark the current page.

A menu inside an item becomes its indented sub-list.

<script lang="ts">
    import Bell from '@lucide/svelte/icons/bell';
    import Clock from '@lucide/svelte/icons/clock';
    import Inbox from '@lucide/svelte/icons/inbox';
    import UserRound from '@lucide/svelte/icons/user-round';
    import * as Sidebar from '@sivir-ui/svelte/components/sidebar';
</script>

<Sidebar.Root collapsible="none" class="h-72 rounded-[var(--radius-xl)] border border-border">
    <Sidebar.Panel>
        <Sidebar.Content>
            <Sidebar.Group>
                <Sidebar.GroupContent>
                    <Sidebar.Menu>
                        <Sidebar.Item>
                            <Sidebar.ItemButton href="#open" active>
                                <Inbox aria-hidden="true" />
                                <Sidebar.ItemLabel>Open</Sidebar.ItemLabel>
                            </Sidebar.ItemButton>
                            <Sidebar.ItemBadge>3</Sidebar.ItemBadge>
                            <Sidebar.Menu>
                                <Sidebar.Item>
                                    <Sidebar.ItemButton href="#assigned">
                                        <UserRound aria-hidden="true" />
                                        <Sidebar.ItemLabel>Assigned to me</Sidebar.ItemLabel>
                                    </Sidebar.ItemButton>
                                    <Sidebar.ItemBadge>1</Sidebar.ItemBadge>
                                </Sidebar.Item>
                                <Sidebar.Item>
                                    <Sidebar.ItemButton href="#subscribed">
                                        <Bell aria-hidden="true" />
                                        <Sidebar.ItemLabel>Subscribed</Sidebar.ItemLabel>
                                    </Sidebar.ItemButton>
                                    <Sidebar.ItemBadge>2</Sidebar.ItemBadge>
                                </Sidebar.Item>
                            </Sidebar.Menu>
                        </Sidebar.Item>
                        <Sidebar.Item>
                            <Sidebar.ItemButton href="#later">
                                <Clock aria-hidden="true" />
                                <Sidebar.ItemLabel>Later</Sidebar.ItemLabel>
                            </Sidebar.ItemButton>
                        </Sidebar.Item>
                    </Sidebar.Menu>
                </Sidebar.GroupContent>
            </Sidebar.Group>
        </Sidebar.Content>
    </Sidebar.Panel>
    <Sidebar.Inset>
        <p class="p-5 text-foreground-muted">
            A menu inside an item becomes its indented sub-list.
        </p>
    </Sidebar.Inset>
</Sidebar.Root>

Right side

Set side="right" on Sidebar.Panel. The panel moves to the end of the row in either source order, and the drawer slides in from the right.

Q3 planning
<script lang="ts">
    import History from '@lucide/svelte/icons/history';
    import Info from '@lucide/svelte/icons/info';
    import MessageSquare from '@lucide/svelte/icons/message-square';
    import * as Sidebar from '@sivir-ui/svelte/components/sidebar';

    const panels = [
        { label: 'Details', icon: Info },
        { label: 'Comments', icon: MessageSquare },
        { label: 'History', icon: History }
    ];
    let active = $state('Details');
</script>

<Sidebar.Root class="h-80 rounded-[var(--radius-xl)] border border-border">
    <Sidebar.Inset>
        <header
            class="flex h-12 shrink-0 items-center justify-between gap-2 border-b border-border px-3"
        >
            <span class="[font-weight:var(--font-weight-header)]">Q3 planning</span>
            <Sidebar.Trigger aria-label="Toggle details" />
        </header>
    </Sidebar.Inset>
    <Sidebar.Panel side="right" aria-label="Document details">
        <Sidebar.Content>
            <Sidebar.Menu class="p-2">
                {#each panels as panel (panel.label)}
                    <Sidebar.Item>
                        <Sidebar.ItemButton
                            active={active === panel.label}
                            onclick={() => (active = panel.label)}
                        >
                            <panel.icon aria-hidden="true" />
                            <Sidebar.ItemLabel>{panel.label}</Sidebar.ItemLabel>
                        </Sidebar.ItemButton>
                    </Sidebar.Item>
                {/each}
            </Sidebar.Menu>
        </Sidebar.Content>
    </Sidebar.Panel>
</Sidebar.Root>

Anatomy

Sidebar.Root - Owns collapse and mobile drawer state.

Sidebar.Panel - Renders the navigation column, or a drawer on small screens.

Sidebar.Inset - Renders the main content area beside the panel.

Sidebar.Trigger - Collapses the panel or opens the drawer.

Sidebar.Header - Holds content pinned to the top of the panel.

Sidebar.Content - Scrolls the groups between header and footer.

Sidebar.Footer - Holds content pinned to the bottom of the panel.

Sidebar.Group - Groups related items, optionally collapsible.

Sidebar.GroupLabel - Labels a group and toggles it when collapsible.

Sidebar.GroupContent - Wraps the items of a group.

Sidebar.Menu - Lists items. Nested inside an item, it indents.

Sidebar.Item - Positions a row with its badge, action, and sub-menu.

Sidebar.ItemButton - Renders the row as a link or a button.

Sidebar.ItemLabel - Renders the truncating row label.

Sidebar.ItemBadge - Shows a trailing count.

Sidebar.ItemAction - Shows a secondary button on hover or focus.