<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { Folder, House } from "@lucide/vue";
</script>
<template>
<SidebarLayout label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/projects" :icon="Folder" tooltip="Projects">Projects</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Installation
Follow the styles and theme setup once in your application. These examples use @lucide/vue for icons. Install it, or use your own Vue icons.
pnpm add @lucide/vueBarrel
import { Sidebar } from "@dicehub/kappa";Granular
import { Sidebar } from "@dicehub/kappa/components/sidebar";Usage
Copy a snippet into a Vue file. The examples use SidebarLayout for the layout, header toggle, mobile Close button, and resize handle. Mount it in an application container with a set height so the navigation can scroll.
For complete application layouts, see Blocks → Sidebar: workspace, icon rail, inset, floating, two-column navigation, and top-header examples.
Put one Root and the application content inside a Provider. Give the layout a height, then let Content scroll between Header and Footer. Keep one Trigger in the application header so the mobile drawer and offcanvas Sidebar can be reopened. A second footer toggle is not needed. These demos align both desktop headers at 4rem. Include Close in the navigation header for mobile.
Use MenuItem around every top-level entry and MenuSubItem around nested entries. These explicit list items keep navigation semantics predictable when you compose a Collapsible.
Use icons for every top-level item when collapsible="icon". Labels remain accessible when visually hidden; tooltip adds a visible label on hover or focus. Custom header and footer content must also fit the rail. Wrap text in MenuLabel, or use Context to change the composition.
For hover reveal, use collapsible="offcanvas" and peekable. Add peek to the external header Trigger. Hover reveals a temporary panel; click or Enter pins it open. Set --kappa-sidebar-peek-top to your header height plus the panel gap, --kappa-sidebar-peek-gap to that gap, and --kappa-sidebar-peek-bottom to the bottom margin. Offsets are relative to the desktop Sidebar shell. The defaults are 0.75rem. Keep the trigger above the panel, next to the same logical edge.
Composition
Sidebar.Provider
├── Sidebar.Root <nav> / Ark Drawer on mobile
│ ├── Sidebar.Header
│ │ ├── Dropdown + MenuButton (namespace, optional)
│ │ └── Sidebar.Close (mobile)
│ ├── Sidebar.Content
│ │ └── Sidebar.Group
│ │ ├── Sidebar.GroupLabel
│ │ └── Sidebar.Menu <ul>
│ │ └── Sidebar.MenuItem <li>
│ │ ├── Sidebar.MenuButton
│ │ └── Sidebar.Collapsible (optional)
│ │ ├── Sidebar.CollapsibleTrigger
│ │ └── Sidebar.CollapsibleContent
│ │ └── Sidebar.MenuSub <ul>
│ ├── Sidebar.Loading (alternative to Content)
│ ├── Sidebar.SlidingViews (optional)
│ │ └── Sidebar.SlidingView → Sidebar.Content
│ ├── Sidebar.Footer
│ │ ├── Dropdown + MenuButton (profile, optional)
│ │ └── Sidebar.Trigger
│ └── Sidebar.ResizeHandle (optional, desktop)
└── application content
└── Sidebar.TriggerUse the primitive parts when you want to supply your own application layout.
<script setup lang="ts">
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<Sidebar.Provider>
<Sidebar.Root label="Workspace navigation">
<Sidebar.Header>Workspace <Sidebar.Close /></Sidebar.Header>
<Sidebar.Content aria-label="Workspace links">
<Sidebar.Menu><Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview">Overview</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.Content>
</Sidebar.Root>
<main><Sidebar.Trigger /><h1>Overview</h1></main>
</Sidebar.Provider>
</template>Sidebar and Sidebar.Root are the same navigation component. Provider supplies the state and flex layout; it does not provide an application header, router, or store.
Kappa owns desktop state, navigation markup, dimensions, and semantic-token styling. Ark owns mobile modality, scroll locking, focus trapping, Escape/outside dismissal, and section accessibility. Mobile panels teleport to body. Global Kappa theme tokens must reach the teleport target.
Resizing adapts Ark Splitter's two-panel size model to a pixel-width Sidebar and normal flex content. No extra application wrapper is required. The separator controls the navigation landmark. Ark's collapse threshold and keyboard behavior are preserved.
Set :collapse-on-resize="false" to stop resizing at minWidth. The separator is hidden while collapsed. The header Trigger can still hide the sidebar.
Start dragging on the divider or a few pixels into the content beside it. The visible line stays on the sidebar border.
The resize scope uses physical left/right panel order, including RTL. This avoids an installed Ark Splitter inconsistency between RTL pointer and keyboard direction without replacing its interaction code. Enter targets the navigation panel through Ark's collapse/expand API when that panel follows the content. Navigation retains the application locale.
Namespace, profile, and Quick search examples compose Dropdown and CommandPalette. They are not application-specific Sidebar parts. The scroll-to-item example uses native scrolling scoped to Content; it does not add a Sidebar scrolling API or move the outer page.
Inside the mobile drawer, keep Dropdown.Content in the drawer with :teleport="false" and fixed positioning. This keeps popup focus inside the modal boundary. Desktop popups can teleport to body.
Offcanvas hover panels stay within the Provider's inline bounds. Teleport desktop menus to body so they can extend beyond those bounds.
Nested Sections
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { Box } from "@lucide/vue";
</script>
<template>
<SidebarLayout label="Project navigation">
<template #header><Sidebar.MenuLabel>Project</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu><Sidebar.MenuItem>
<Sidebar.Collapsible default-open>
<Sidebar.CollapsibleTrigger as-child>
<Sidebar.MenuButton :icon="Box" tooltip="Mesh">Mesh <Sidebar.MenuChevron /></Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent>
<Sidebar.MenuSub><Sidebar.MenuSubItem>
<Sidebar.MenuButton href="/mesh/geometry">Geometry</Sidebar.MenuButton>
</Sidebar.MenuSubItem></Sidebar.MenuSub>
</Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem></Sidebar.Menu>
</template>
<h1>Geometry</h1>
</SidebarLayout>
</template>Icon collapse hides nested content without resetting each section's open preference. Activating a collapsed section requests expansion of the Sidebar and section. Controlled parents must accept both updates when both levels are controlled.
Examples
Each snippet is a complete Vue file for the named feature. Previews include additional menus and controls.
Namespace Selector
Current workspace details, account actions, and a workspace list with the selected checkmark on the right. Ctrl/⌘ + 1–3 works only while the menu is open. Mobile menus stay inside the drawer.
<script setup lang="ts">
import { ref } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { WorkspaceSwitcher } from "@dicehub/kappa/blocks/workspace-switcher";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { Building2, FlaskConical } from "@lucide/vue";
const namespace = ref("Engineering");
const namespaces = [
{ value: "Engineering", name: "Engineering", description: "12 members", icon: Building2 },
{ value: "Research", name: "Research", description: "8 members", icon: FlaskConical },
];
</script>
<template>
<SidebarLayout label="Workspace navigation">
<template #header="{ isMobile }">
<WorkspaceSwitcher v-model="namespace" :items="namespaces" label="Namespaces" :teleport="!isMobile">
<template #trigger="{ workspace }">
<Sidebar.MenuButton :icon="workspace?.icon" :tooltip="namespace" :aria-label="`Namespace: ${namespace}`">
{{ namespace }}
</Sidebar.MenuButton>
</template>
</WorkspaceSwitcher>
</template>
<template #navigation>
<Sidebar.Menu><Sidebar.MenuItem>
<Sidebar.MenuButton :icon="Building2" tooltip="Overview" href="/overview">Overview</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</template>
<h1>{{ namespace }}</h1>
</SidebarLayout>
</template>Profile Selector
A footer menu with initials, name, email, profile switching, and account actions. All data is synthetic. Actions update this example only.
<script setup lang="ts">
import { ref } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { Dropdown } from "@dicehub/kappa/components/dropdown";
import { UserRound } from "@lucide/vue";
const profile = ref("Casey Rivera");
const profiles = ["Casey Rivera", "Jordan Lee"];
</script>
<template>
<SidebarLayout label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #footer="{ isMobile }">
<Dropdown.Root :positioning="{ placement: 'top-start', strategy: 'fixed' }">
<Dropdown.Trigger as-child>
<Sidebar.MenuButton :icon="UserRound" :tooltip="profile">{{ profile }}</Sidebar.MenuButton>
</Dropdown.Trigger>
<Dropdown.Content :teleport="!isMobile">
<Dropdown.Label>Switch profile</Dropdown.Label>
<Dropdown.Item v-for="name in profiles" :key="name" :value="name" :selected="profile === name"
:aria-current="profile === name ? 'true' : undefined" @select="profile = name">{{ name }}</Dropdown.Item>
</Dropdown.Content>
</Dropdown.Root>
</template>
<h1>{{ profile }}</h1>
</SidebarLayout>
</template>Quick Search
Quick search opens a real CommandPalette. Type a page name, use the arrow keys, then press Enter to navigate. Escape returns focus to the search button. No global shortcut is registered.
<script setup lang="ts">
import { ref } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { CommandPalette } from "@dicehub/kappa/components/command-palette";
import { Search } from "@lucide/vue";
const open = ref(false);
const selected = ref("Overview");
const pages = ["Overview", "Projects", "Settings"];
</script>
<template>
<SidebarLayout label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu><Sidebar.MenuItem>
<Sidebar.MenuButton :icon="Search" tooltip="Quick search" @click="open = true">Quick search</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</template>
<h1>{{ selected }}</h1>
<CommandPalette.Root v-model:open="open" :items="pages" aria-label="Search navigation"
@select="item => { selected = String(item); open = false; }">
<CommandPalette.Input placeholder="Search navigation…" />
<CommandPalette.List>
<CommandPalette.Results v-slot="{ item }">
<CommandPalette.Item :value="item">{{ item }}</CommandPalette.Item>
</CommandPalette.Results>
<CommandPalette.Empty>No pages found.</CommandPalette.Empty>
</CommandPalette.List>
</CommandPalette.Root>
</SidebarLayout>
</template>Resizable
Drag the separator to change the width. Drag toward the rail to collapse; drag outward to expand. Arrow keys resize, Shift increases the step, Home/End move to the edge limits, and Enter toggles collapse. Ark owns the drag threshold between the minimum width and the rail. The handle is desktop-only.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<SidebarLayout resizable :default-width="240" :min-width="180" :max-width="400" label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Controlled Width
Keep the expanded width in application state. Lock width to reject resize requests. Collapse and mobile state remain separate.
<script setup lang="ts">
import { ref } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
const width = ref(240);
</script>
<template>
<SidebarLayout resizable v-model:resize-width="width" :min-width="180" :max-width="400" label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Peeking
Hover or focus the collapsed rail to peek. The live state distinguishes a temporary peek from a pinned sidebar. Pin it open, then collapse it to try again. Namespace and profile menus keep the peek visible. Escape or leaving both the Sidebar and its popup closes it without moving the page.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<SidebarLayout peekable :default-open="false" label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Hover Reveal
Collapse the sidebar, then hover over the header toggle. Click to pin it open. Drag the pinned divider to resize within the width limits. Use Home, AI Chat, Projects, or Inbox to change the navigation below. Every context label appears as soon as the full row fits. Otherwise, only the selected label is shown. Context changes keep the current page and group state. Hover over the workspace name to show its menu icon. Its background stays highlighted while the menu is open. Use the search icon or press / to find a page across contexts. The shortcut works in the full example, or while focus is inside this preview. Escape closes menus and search before dismissing the panel. On mobile, the toggle opens a drawer.
The snippet adds hover reveal, pinning, and resizing. Use WorkspaceSwitcher, Tabs, and CommandPalette to add the preview's other controls.
The preview leaves room for its content when resizing. Open the full-screen example to try screen-centered content.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<SidebarLayout variant="header" collapsible="offcanvas" peekable resizable
:min-width="260" :max-width="600" :collapse-on-resize="false"
:trigger-props="{ peek: true }" label="Workspace navigation">
<template #header><Sidebar.Trigger /> Workspace</template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Scroll to Item
Jump to an item in a long navigation list without moving the documentation page or keyboard focus. Keep Settings visible does nothing when that row is already fully visible. This composition uses native Content scrolling; reduced motion disables smooth scrolling.
The snippet scrolls the navigation to Settings instantly. Its handler changes only the Sidebar's scroll position.
<script setup lang="ts">
import { ref } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { Button } from "@dicehub/kappa/components/button";
import { FileText, Settings } from "@lucide/vue";
const settings = ref<HTMLElement>();
const reports = Array.from({ length: 40 }, (_, index) => `Report ${index + 1}`);
function scrollToSettings() {
const item = settings.value;
const content = item?.closest<HTMLElement>('[data-slot="sidebar-content"]');
if (!item || !content) return;
const top = content.scrollTop + item.getBoundingClientRect().top - content.getBoundingClientRect().top;
content.scrollTo({ top, behavior: "instant" });
}
</script>
<template>
<SidebarLayout collapsible="none" :mobile-breakpoint="0" label="Report navigation">
<template #header><Sidebar.MenuLabel>Reports</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem v-for="report in reports" :key="report">
<Sidebar.MenuButton :icon="FileText" :tooltip="report">{{ report }}</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem as-child>
<li ref="settings"><Sidebar.MenuButton :icon="Settings">Settings</Sidebar.MenuButton></li>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<Button @click="scrollToSettings">Scroll to Settings</Button>
</SidebarLayout>
</template>Sliding Views
Use the navigation header or the page button to switch between workspace and project views. You can also open Rotor study from the list and go back. The active view is shown beside the navigation. Inactive views stay mounted, hidden, and inert. Motion respects reduced-motion settings.
<script setup lang="ts">
import { ref } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { ArrowLeft, Folder } from "@lucide/vue";
const view = ref("workspace");
</script>
<template>
<SidebarLayout label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.SlidingViews :active-key="view" :direction="view === 'project' ? 'left' : 'right'">
<Sidebar.SlidingView value="workspace" label="Workspace view">
<Sidebar.Menu><Sidebar.MenuItem>
<Sidebar.MenuButton :icon="Folder" tooltip="Rotor study" @click="view = 'project'">Rotor study</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.SlidingView>
<Sidebar.SlidingView value="project" label="Project view">
<Sidebar.Menu><Sidebar.MenuItem>
<Sidebar.MenuButton :icon="ArrowLeft" tooltip="Back to workspace" @click="view = 'workspace'">Back to workspace</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</Sidebar.SlidingView>
</Sidebar.SlidingViews>
</template>
<h1>Rotor study</h1>
</SidebarLayout>
</template>Compact
28px desktop rows for dense navigation. Mobile rows keep 44px touch targets.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<SidebarLayout compact label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Icon Collapse
Start with an icon rail. Tooltips appear on hover and keyboard focus. Activating a nested section expands the rail.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<SidebarLayout :default-open="false" label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Offcanvas
Hide the entire desktop Sidebar. Keep a Trigger outside it so users can reopen it.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<SidebarLayout collapsible="offcanvas" label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Non-collapsible
Keep desktop navigation expanded. Small viewports still use the accessible mobile drawer.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<SidebarLayout collapsible="none" label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Controlled State
Control desktop and mobile state separately. Lock state to test a parent that rejects a desktop change request.
<script setup lang="ts">
import { ref } from "vue";
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
const open = ref(true);
const mobileOpen = ref(false);
</script>
<template>
<SidebarLayout v-model:open="open" v-model:mobile-open="mobileOpen" label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>End Placement
Logical end placement follows the text direction. Drag the separator left to grow this Sidebar. Home grows it; End collapses it, following Ark panel order.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<SidebarLayout side="end" resizable :default-width="240" label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Right-to-left
Use DirectionProvider for Ark behavior and dir for text and layout. Start becomes the right edge.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { DirectionProvider } from "@dicehub/kappa/components/direction-provider";
import { House } from "@lucide/vue";
</script>
<template>
<DirectionProvider locale="ar">
<SidebarLayout dir="rtl" label="التنقل الرئيسي" resizable>
<template #header><Sidebar.MenuLabel>المساحة</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu><Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="نظرة عامة">نظرة عامة</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</template>
<h1>نظرة عامة</h1>
</SidebarLayout>
</DirectionProvider>
</template>Mobile Drawer
Open the drawer inside this resizable viewport. It starts at the preview's left edge, not at the edge of the documentation page. Tab stays inside the drawer. Escape, Close, and the backdrop dismiss it. Open example tests the same composition in a full browser tab.
The snippet uses the normal mobile breakpoint. Resize your browser below 768 px to open the drawer.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<SidebarLayout label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Full-screen Mobile
Resize the viewport, then open the full-screen navigation. The drawer fills only that viewport, so the documentation stays visible. Try namespace and profile menus, Quick search, and a nested page such as Refinement. Its breadcrumb remains visible after the drawer closes.
The snippet uses the normal mobile breakpoint. Resize your browser below 768 px to open the drawer.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
</script>
<template>
<SidebarLayout full-screen-on-mobile label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview" active>Overview</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Long Navigation
Header and Footer stay visible while Content scrolls. Long labels do not change the width.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { FileText } from "@lucide/vue";
const reports = Array.from({ length: 40 }, (_, index) => `Report ${index + 1}`);
</script>
<template>
<SidebarLayout label="Report navigation">
<template #header><Sidebar.MenuLabel>Reports</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu><Sidebar.MenuItem v-for="report in reports" :key="report">
<Sidebar.MenuButton :icon="FileText" :tooltip="report">{{ report }}</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</template>
<template #footer><Sidebar.MenuLabel>Casey Rivera</Sidebar.MenuLabel></template>
<h1>Reports</h1>
</SidebarLayout>
</template>Loading
Deterministic placeholder rows with an accessible loading status. Motion stops under reduced-motion preferences.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { House } from "@lucide/vue";
defineProps<{ loading?: boolean }>();
</script>
<template>
<SidebarLayout label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Loading v-if="loading" :rows="5" />
<Sidebar.Menu v-else><Sidebar.MenuItem>
<Sidebar.MenuButton href="/overview" :icon="House" tooltip="Overview">Overview</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</template>
<h1>Overview</h1>
</SidebarLayout>
</template>Routing and State
Supply href for normal links, or compose a router link with as-child. The library does not read the URL or select routes. Set active from your route state. For custom children, put the icon and MenuLabel inside the link.
MenuButton does not close the mobile drawer automatically. Call setMobileOpen(false) after your router accepts the navigation. This keeps rejected and asynchronous navigation under application control.
<script setup lang="ts">
import { SidebarLayout } from "@dicehub/kappa/blocks/sidebar-layout";
import { Sidebar } from "@dicehub/kappa/components/sidebar";
import { Folder } from "@lucide/vue";
defineProps<{ currentPath?: string }>();
</script>
<template>
<SidebarLayout label="Workspace navigation">
<template #header><Sidebar.MenuLabel>Workspace</Sidebar.MenuLabel></template>
<template #navigation>
<Sidebar.Menu><Sidebar.MenuItem>
<Sidebar.MenuButton as-child tooltip="Projects" :active="currentPath === '/projects'">
<a href="/projects">
<Folder aria-hidden="true" />
<Sidebar.MenuLabel>Projects</Sidebar.MenuLabel>
</a>
</Sidebar.MenuButton>
</Sidebar.MenuItem></Sidebar.Menu>
</template>
<h1>Projects</h1>
</SidebarLayout>
</template>Provider supports v-model:open and v-model:mobile-open. Uncontrolled defaults are read once. Leaving mobile mode requests that mobile state close; the desktop preference is unchanged. Controlled parents must handle update events. No cookies, storage, global keyboard shortcuts, or application state are added.
SSR renders the desktop structure, then checks the viewport after hydration. Navigation content remounts when switching between desktop and mobile. Keep persistent form state and controlled section state outside Root when they must survive that transition.
Accessibility
- Root is a named navigation landmark, not an ARIA menu. Use Tab and Shift+Tab to move between links; Enter activates links, and Enter or Space activates buttons and disclosures.
- Active entries use
aria-current="page". Disabled entries cannot navigate or activate and leave the tab order. - Mobile uses a named modal dialog. Focus stays inside while open. Escape, Close, and backdrop interaction dismiss it; focus returns to the opening Trigger.
- Use a visible Close in mobile headers. Do not rely only on the backdrop.
- Collapsed offcanvas content is hidden and inert. A temporary hover panel exposes its links. Icon labels remain in the accessibility tree; nested hidden links leave the tab order.
- ResizeHandle is a focusable vertical separator. Arrow keys, Shift+Arrow, Home, End, and Enter use Ark's resize behavior. End placement reverses which edge grows the Sidebar. The handle is not rendered on mobile.
- Peeking keeps the desktop layout width fixed. Escape dismisses it without moving the pointer. It closes when pointer and focus leave and its popups are closed. Popups linked through aria-controls keep the panel available; their first Escape closes the popup.
- Sliding views keep inactive content mounted, hidden, and inert. In-view navigation transfers focus; an external view change does not take focus from its external control.
- Content is a keyboard-focusable scrolling region. Use a descriptive aria-label if its default is not suitable.
- Pass translated Root, Trigger, Close, and Loading labels. Pair DirectionProvider with a DOM dir attribute for RTL.
- Motion respects reduced-motion preferences. Mobile navigation controls keep a 44px minimum height, including compact mode.
API Reference
Sidebar.Provider
| Prop | Type | Default | Description |
|---|---|---|---|
open / defaultOpen | boolean | undefined / true | Controlled or initial desktop expanded state. |
mobileOpen / defaultMobileOpen | boolean | undefined / false | Independent controlled or initial mobile drawer state. |
collapsible | "icon" | "offcanvas" | "none" | "icon" | Desktop collapse mode. none ignores desktop collapse requests. |
side | "start" | "end" | "start" | Logical edge. End placement orders the desktop Sidebar after the application content. |
compact | boolean | false | 28px instead of 32px minimum desktop rows. Mobile remains 44px. |
mobileBreakpoint | number | 768 | Viewport widths below this many pixels use a drawer. Zero disables mobile mode. |
width / collapsedWidth / mobileWidth | CSS length | "16.25rem" / "3.25rem" / "18rem" | Expanded desktop, icon rail, and mobile widths. Mobile width leaves at least 3rem for the backdrop. |
resizable | boolean | false | Enable desktop resizing through ResizeHandle. The numeric resize width replaces the fixed width prop. |
collapseOnResize | boolean | true | Allow the separator to collapse navigation. Set false to stop at minWidth and hide the separator during collapse. Sidebar.Trigger can still hide the sidebar. |
resizeWidth / defaultWidth | number (px) | undefined / 260 | Controlled or initial expanded resize width. Resize emits requests; a controlled parent can reject them. |
minWidth / maxWidth | number (px) | 180 / 400 | Expanded resize bounds. A collapsed icon rail can be smaller than minWidth. |
peekable | boolean | false | Temporarily reveal a collapsed icon rail on hover/focus, or offcanvas navigation through a peek Trigger. Desktop only; ignored in none mode. |
id | string | Vue useId() | Stable ID prefix for navigation and dialog relationships. Set IDs here, not on Root. |
Parts
| Part | Props | Default | Description |
|---|---|---|---|
Root | label | "Main navigation" | Accessible navigation and mobile dialog name. Give multiple Sidebars distinct names. |
Root | fullScreenOnMobile | false | Mobile dialog covers the viewport. |
Trigger | expandLabel / collapseLabel / openLabel / closeLabel | English action labels | Optional localized labels for the built-in icon. Custom content must provide its own accessible name. |
Trigger | disabled / asChild | false | Disable the action or compose it with a custom button. |
Trigger | peek | false | Hover reveals peekable offcanvas navigation. Click or Enter pins it open. aria-expanded includes temporary visibility. |
Close | label / asChild | "Close sidebar" / false | Mobile-only close action. Hidden on desktop. |
MenuButton | href / asChild | undefined / false | A native link when href is set; otherwise a button. asChild accepts a router link or native element. |
MenuButton | active / disabled | false | Current-page styling and aria-current; disabled controls block activation and leave the tab order. |
MenuButton | icon / tooltip | undefined | Vue icon component (or #icon slot), and optional collapsed-rail tooltip. |
Collapsible | open / defaultOpen / disabled / id | undefined / false / false / generated | Ark disclosure behavior with controlled and uncontrolled state. Sections retain their open preference during icon collapse. |
CollapsibleTrigger / CollapsibleContent | asChild | false | Compose Ark parts with Sidebar.MenuButton or a custom element. |
Loading | rows / label | 5 / "Loading navigation" | 1–20 placeholder rows and a localized accessible status name. |
ResizeHandle | label / disabled | "Resize sidebar" / false | Focus-visible desktop separator. Ark handles pointer and keyboard resizing. In offcanvas mode, reopen with an external Trigger. |
SlidingViews | activeKey / direction | required / "left" | Controlled active view key and physical transition direction (left or right). Reverse direction for back navigation. |
SlidingView | value / label | required / value | Matching view key and accessible group name. Inactive views stay mounted but are hidden and inert. |
Structural parts | asChild | false | Header, Footer, Content, Group, GroupLabel, Menu, MenuItem, MenuSub, MenuSubItem, MenuLabel, MenuBadge, and Separator forward attributes to their semantic element. |
Events and Context
Provider emits update:open(boolean), openChange({ open }), update:mobileOpen(boolean), and mobileOpenChange({ open }). Collapsible emits update:open and openChange. Controlled components emit requests without changing the supplied value.
Resizing emits update:resizeWidth(number), resize({ width }), and resizeEnd({ width }). Widths are in pixels. The expanded width is retained during collapse and mobile mode. Use v-model:resize-width to control it.
Sidebar.Context exposes unwrapped open, mobileOpen, isMobile, state, iconCollapsed, side, compact, and collapsible, plus setOpen, setMobileOpen, and toggle. useSidebarContext() exposes the same values as Vue refs in a descendant setup function.
Context also exposes resizable, width, isResizing, setWidth, peekable, and isPeeking. State can be expanded, collapsed, or peeking. Peeking never emits a desktop open change.
Use focusTrigger() to return focus to a visible Sidebar Trigger.
A peek Trigger reports visible hover navigation through aria-expanded. Its click toggles persistent open. Keyboard focus alone does not reveal offcanvas navigation. Escape returns focus to a visible Trigger when focus was inside the panel; page focus stays in place during pointer-only use.
Styling and Exports
Structural parts forward attributes, classes, styles, and listeners. Root and Provider use reserved generated IDs; supply the Provider id prop to customize their relationships. Root exposes data-state, data-side, data-collapsible, data-mobile, and data-compact. MenuButton exposes data-active and data-disabled; Ark parts retain their state attributes.
Colors inherit Kappa control, default, subtle, line, tint, overlay, and focus tokens. Dimensions use --kappa-sidebar-width, --kappa-sidebar-collapsed-width, and --kappa-sidebar-mobile-width; prefer the matching Provider props so teleported mobile content receives them too.
Every compound part has a named export, such as SidebarProvider, SidebarRoot, and SidebarMenuButton. Public props, slots, state, events, SidebarContextValue, and SIDEBAR_DEFAULTS are exported from the same subpath.