<script setup>
import { Sidebar } from "@dicehub/phi/components/sidebar";
import { PhHouse, PhGlobe } from "@phosphor-icons/vue";
</script>
<template>
<Sidebar.Provider default-open>
<Sidebar>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Overview</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuButton :icon="PhHouse" href="/docs" active aria-current="location">Home</Sidebar.MenuButton>
<Sidebar.MenuButton :icon="PhGlobe">Domains</Sidebar.MenuButton>
</Sidebar.Menu>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar>
</Sidebar.Provider>
</template>Installation
Barrel
import { Sidebar } from "@dicehub/phi";Granular
import { Sidebar } from "@dicehub/phi/components/sidebar";Usage
At minimum you need Provider, Sidebar, Content, Menu, and MenuButton. Add Header and Footer to pin content above or below the scroll area.
<script setup>
import { Sidebar } from "@dicehub/phi/components/sidebar";
import { PhCode, PhHouse } from "@phosphor-icons/vue";
</script>
<template>
<Sidebar.Provider default-open>
<Sidebar>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Navigation</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuButton :icon="PhHouse" active>Home</Sidebar.MenuButton>
<Sidebar.MenuItem>
<Sidebar.Collapsible>
<Sidebar.CollapsibleTrigger>
<Sidebar.MenuButton :icon="PhCode">
Compute <Sidebar.MenuChevron />
</Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent>
<Sidebar.MenuSub>
<Sidebar.MenuSubButton href="/docs/components/sidebar" active aria-current="step">Workers</Sidebar.MenuSubButton>
</Sidebar.MenuSub>
</Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer>
<Sidebar.Trigger />
</Sidebar.Footer>
</Sidebar>
<main>Page content</main>
</Sidebar.Provider>
</template>Examples
Loading
Use Sidebar.Loading while routes, permissions, or navigation data resolve. It mirrors menu-row geometry, collapses to icon squares, and inherits SkeletonLine reduced-motion behavior.
<script setup>
import { ref } from "vue";
const isLoading = ref(true);
</script>
<template>
<Sidebar>
<Sidebar.Header>...</Sidebar.Header>
<Sidebar.Loading v-if="isLoading" />
<Sidebar.Content v-else>...</Sidebar.Content>
<Sidebar.Footer>...</Sidebar.Footer>
</Sidebar>
</template>Basic
The minimum viable sidebar: just groups, menu buttons, and collapsible sub-menus. No header or footer. MenuButton and MenuSubButton auto-wrap in li.
<script setup>
import { Sidebar } from "@dicehub/phi/components/sidebar";
import { PhHouse, PhGlobe } from "@phosphor-icons/vue";
</script>
<template>
<Sidebar.Provider default-open>
<Sidebar>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Overview</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuButton :icon="PhHouse" href="/docs" active aria-current="location">Home</Sidebar.MenuButton>
<Sidebar.MenuButton :icon="PhGlobe">Domains</Sidebar.MenuButton>
</Sidebar.Menu>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar>
</Sidebar.Provider>
</template>Toggle & Collapsed State
Use Sidebar.Trigger or useSidebar().toggleSidebar programmatically. Pass tooltip to show labels on hover when collapsed.
<template>
<Sidebar.Provider default-open>
<Sidebar>
<Sidebar.Content>
<Sidebar.Menu>
<Sidebar.MenuButton :icon="PhHouse" tooltip="Home" active>
Home
</Sidebar.MenuButton>
</Sidebar.Menu>
</Sidebar.Content>
<Sidebar.Footer>
<Sidebar.Trigger />
</Sidebar.Footer>
</Sidebar>
</Sidebar.Provider>
</template>Resizable
Drag the edge to resize. Dragging below minWidth collapses; dragging outward from collapsed expands. The resize handle is keyboard accessible: use arrow keys, Home, and End.
<template>
<Sidebar.Provider default-open resizable :default-width="240" :min-width="180" :max-width="400">
<Sidebar>
<Sidebar.Content>...</Sidebar.Content>
<Sidebar.ResizeHandle />
</Sidebar>
</Sidebar.Provider>
</template>Right Side
Use side="right" for a sidebar on the right edge. Place main before Sidebar in the DOM.
<template>
<Sidebar.Provider default-open side="right">
<main>...</main>
<Sidebar>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Details</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuButton :icon="PhGear" active>Properties</Sidebar.MenuButton>
</Sidebar.Menu>
</Sidebar.Group>
</Sidebar.Content>
</Sidebar>
</Sidebar.Provider>
</template>Peeking
Set peekable on the provider. When collapsed, hover or focus temporarily expands the sidebar and sets data-state="peeking".
<template>
<Sidebar.Provider default-open peekable>
<Sidebar>
<Sidebar.Content>...</Sidebar.Content>
<Sidebar.Footer>
<Sidebar.Trigger />
</Sidebar.Footer>
</Sidebar>
</Sidebar.Provider>
</template>Auto Scroll
Use autoScrollOnOpen on long collapsible sections to keep newly revealed content in view.
<template>
<Sidebar.Collapsible auto-scroll-on-open>
<Sidebar.CollapsibleTrigger>
<Sidebar.MenuButton :icon="PhCode">
Workers <Sidebar.MenuChevron />
</Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent>
<Sidebar.MenuSub>...</Sidebar.MenuSub>
</Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</template>Transition Completion
Use @open-change-complete when a layout must react after the animation settles, for example to measure the sidebar or to scroll newly available space. The event fires once per settled state, never during initial mount, and completes immediately when the duration is zero or reduced motion is active.
<script setup>
import { ref } from "vue";
// Fires when the open/close animation settles, not when the state changes.
const sidebarSettled = ref<boolean | null>(null);
const sectionSettled = ref<boolean | null>(null);
</script>
<template>
<Sidebar.Provider default-open @open-change-complete="sidebarSettled = $event">
<Sidebar.Collapsible @open-change-complete="sectionSettled = $event">
<Sidebar.CollapsibleTrigger>
<Sidebar.MenuButton :icon="PhCode">
Compute <Sidebar.MenuChevron />
</Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent>
<Sidebar.MenuSub>...</Sidebar.MenuSub>
</Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.Provider>
</template>Sliding Views
Use Sidebar.SlidingViews and Sidebar.SlidingView for animated horizontal transitions between navigation surfaces. Inactive views are marked with aria-hidden and inert.
<script setup>
import { ref } from "vue";
const surface = ref("account");
</script>
<template>
<Sidebar.SlidingViews :active-key="surface" direction="left">
<Sidebar.SlidingView value="account">
<Sidebar.Content>...account nav...</Sidebar.Content>
</Sidebar.SlidingView>
<Sidebar.SlidingView value="zone">
<Sidebar.Content>...zone nav...</Sidebar.Content>
</Sidebar.SlidingView>
</Sidebar.SlidingViews>
</template>Full Example
Kitchen sink showcasing header content, groups with labels, nested collapsibles, badges, sliding views, and a footer trigger.
<template>
<Sidebar>
<Sidebar.Header>
<AccountSwitcher />
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.Menu>
<Sidebar.MenuButton :icon="PhHouse" active>Home</Sidebar.MenuButton>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Build</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.Collapsible default-open>
<Sidebar.CollapsibleTrigger>
<Sidebar.MenuButton :icon="PhCode">
Compute <Sidebar.MenuChevron />
</Sidebar.MenuButton>
</Sidebar.CollapsibleTrigger>
<Sidebar.CollapsibleContent>
<Sidebar.MenuSub>
<Sidebar.MenuSubButton>
Containers <Sidebar.MenuBadge>Beta</Sidebar.MenuBadge>
</Sidebar.MenuSubButton>
</Sidebar.MenuSub>
</Sidebar.CollapsibleContent>
</Sidebar.Collapsible>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer>
<Sidebar.Trigger />
</Sidebar.Footer>
</Sidebar>
</template>Mobile
On narrow viewports the sidebar renders as a navigation drawer. Use mobileBreakpoint to control the threshold. The drawer uses inert and aria-hidden while closed, moves focus in on open, stays open for portaled interactive content, and supports Escape-to-close.
<template>
<Sidebar.Provider :mobile-breakpoint="9999">
<Sidebar>
<Sidebar.Content>...</Sidebar.Content>
</Sidebar>
</Sidebar.Provider>
</template>Full-screen Mobile
Pass fullScreenOnMobile to cover the mobile viewport instead of leaving page content visible. The backdrop and divider are suppressed; add Sidebar.Close inside the header and keep route breadcrumbs in the page header.
<template>
<Sidebar.Provider :mobile-breakpoint="9999">
<Sidebar full-screen-on-mobile>
<Sidebar.Header>
<Breadcrumbs size="sm">...</Breadcrumbs>
<Sidebar.Close />
</Sidebar.Header>
<Sidebar.Content>...</Sidebar.Content>
</Sidebar>
<header>
<Sidebar.Trigger />
<Breadcrumbs size="sm">...</Breadcrumbs>
</header>
</Sidebar.Provider>
</template>Scrolling to Items
Set a unique itemId on Sidebar.MenuItem or Sidebar.MenuButton. Call useSidebar().scrollItemIntoView(id, options) to reveal an item only when it is outside its viewport. A fully visible item stays in place, even with explicit alignment. Use scrollToItem to align an item even when it is already visible.
Both methods scroll only the owning Sidebar.Content viewport and support scaled containers. Missing or unmounted items have no effect. Use the default align: "auto" for the minimum scroll distance, or choose "start", "center", or "end".behavior: "smooth" respects reduced motion. Item IDs must be unique within each provider.
<script setup lang="ts">
import { Sidebar, useSidebar } from "@dicehub/phi/components/sidebar";
// Call inside a component rendered below Sidebar.Provider.
const { scrollItemIntoView, scrollToItem } = useSidebar();
const reveal = () => scrollItemIntoView("settings", { align: "center" });
const align = () => scrollToItem("settings", { align: "start" });
</script>
<template>
<Sidebar.Content>
<Sidebar.Menu>
<Sidebar.MenuButton item-id="settings">Settings</Sidebar.MenuButton>
</Sidebar.Menu>
</Sidebar.Content>
<button @click="reveal">Reveal settings if outside the viewport</button>
<button @click="align">Align settings with the top</button>
</template>API Reference
Sidebar
The rendered sidebar container. Top-level state and layout props live on Sidebar.Provider.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
default slot | slot | - | Header, Content, Footer, Rail, ResizeHandle, and other sidebar parts. |
contentClass / contentClassName | string | object | array | - | Additional classes applied to the desktop content container. |
fullScreenOnMobile | boolean | false | Expands the mobile drawer to the full viewport while suppressing its backdrop and divider. |
class | string | object | array | - | Additional classes applied to the sidebar rail. |
Sidebar.Provider
Context provider managing expand/collapse state, mobile detection, peeking, and resizing.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
default slot | slot | - | Typically contains Sidebar, page content, and mobile trigger controls. |
defaultOpen | boolean | true | Initial open state when uncontrolled. |
open | boolean | - | Controlled open state. |
modelValue / v-model | boolean | - | Vue controlled open state alias. |
variant | "sidebar" | "floating" | "inset" | "sidebar" | Sidebar layout variant. |
side | "left" | "right" | "left" | Which side the sidebar is on. |
collapsible | "icon" | "offcanvas" | "none" | "icon" | Collapse behavior. |
resizable | boolean | false | Enables drag-to-resize on the sidebar edge. |
defaultWidth | number | 256 | Initial width in pixels when resizable. |
minWidth | number | 200 | Minimum width in pixels when resizing. |
maxWidth | number | 480 | Maximum width in pixels when resizing. |
contained | boolean | false | Keeps collapsed overlays and mobile drawer scoped inside a bounded parent. |
peekable | boolean | false | Hovering or focusing a collapsed sidebar temporarily expands it. |
animationDuration | number | 250 | Expand/collapse animation duration in milliseconds. |
mobileBreakpoint | number | 768 | Viewport width below which the sidebar renders as a mobile drawer. |
class | string | object | array | - | Additional classes applied to the provider wrapper. |
@open-change | (open: boolean) => void | - | Emitted when open state changes. |
@open-change-complete | (open: boolean) => void | - | Emitted once the width or transform transition settles in the current layout. Zero duration, reduced motion, and missing transition events complete without waiting. |
@width-change | (width: number) => void | - | Emitted when width changes during resize. |
@update:open | (open: boolean) => void | - | Controlled open update event. |
@update:model-value | (open: boolean) => void | - | v-model update event. |
Sidebar.Content
Scrollable middle section. Use Sidebar.Header and Sidebar.Footer to pin content above or below this area.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
default slot | slot | - | Navigation groups, menus, and other scrollable sidebar content. |
class | string | object | array | - | Additional classes applied to the content root. |
Sidebar.Loading
Navigation-shaped loading state composed from SkeletonLine. Render it in place of Sidebar.Content while navigation data resolves.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
label | string | "Loading" | Accessible label announced for the status region. |
class | string | object | array | - | Additional classes forwarded to the loading-state root. |
Sidebar.MenuButton
Primary interactive element. Supports icons, active state, links, and collapsed tooltips.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
itemId | string | - | Unique item ID for scrollToItem and scrollItemIntoView. Also supported on Sidebar.MenuItem. |
default slot | slot | - | Visible label and optional trailing elements such as Sidebar.MenuChevron or Sidebar.MenuBadge. |
icon | Component | - | Vue icon component rendered before the label. |
active | boolean | false | Marks this item as active and sets data-active. |
size | "base" | "sm" | "base" | Button size. |
href | string | - | When set, renders as an anchor instead of a button. |
target | string | - | Anchor target when href is set. |
aria-current | string | boolean | "page" when active | Explicit native link value is preserved; active links otherwise default to page. |
tooltip | string | - | Native title tooltip shown when collapsed and not peeking. |
disabled | boolean | false | Disables the button when rendered as button. |
class | string | object | array | - | Additional classes applied to the button or link. |
Sidebar.MenuSubButton
Button inside a sub-menu. Auto-wraps in li when not already inside Sidebar.MenuSubItem.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
default slot | slot | - | Visible label and optional trailing elements. |
active | boolean | false | Marks this sub-item as active and sets data-active. |
href | string | - | When set, renders as an anchor instead of a button. |
target | string | - | Anchor target when href is set. |
aria-current | string | boolean | "page" when active | Explicit native link value is preserved; active links otherwise default to page. |
disabled | boolean | false | Disables the button when rendered as button. |
Sidebar Compound Parts
Public compound parts exported on Sidebar for Vue slot composition.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
Sidebar.Header | component | - | Fixed header region above the scrollable navigation content. |
Sidebar.Content | component | - | Scrollable main navigation region. |
Sidebar.Footer | component | - | Fixed footer region below the scrollable navigation content. |
Sidebar.Loading | component | - | Status region with grouped navigation-row skeletons. |
Sidebar.Group | component | - | Groups a label and related menu items. |
Sidebar.GroupLabel | component | - | Subtle group heading that hides in icon-only collapsed state. |
Sidebar.Menu | component | - | Menu list container. |
Sidebar.MenuItem | component | - | Explicit li wrapper when a menu button should not auto-wrap. |
Sidebar.MenuBadge | component | - | Trailing badge for counts or labels. |
Sidebar.MenuSub | component | - | Indented nested menu list. |
Sidebar.MenuSubItem | component | - | Explicit li wrapper for nested menu buttons. |
Sidebar.Separator | component | - | Visual separator between sidebar sections. |
Sidebar.Trigger | component | - | Button that toggles the sidebar and reflects aria-expanded. |
Sidebar.Close | component | - | Ghost button that closes the mobile drawer. Intended for full-screen mobile headers. |
Sidebar.Rail | component | - | Invisible edge hit target that toggles collapsed sidebars. |
Sidebar.ResizeHandle | component | - | Keyboard and pointer resize handle shown when Sidebar.Provider is resizable. |
Sidebar.MenuChevron | component | - | Chevron that rotates from the nearest Sidebar.Collapsible state. |
Sidebar.CollapsibleTrigger | component | - | Clones its default slot and merges trigger attributes. |
Sidebar.CollapsibleContent | component | - | Animated collapsible content panel. |
Sidebar.SlidingView | component | - | Individual panel inside Sidebar.SlidingViews; inactive panels are aria-hidden and inert. |
useSidebar | composable | - | Reads sidebar context, including scrollToItem(id, options) and scrollItemIntoView(id, options). Must be called inside Sidebar.Provider. |
Sidebar.Collapsible
Collapsible wrapper for sidebar sub-menu expand/collapse.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | false | Initial open state when uncontrolled. |
open | boolean | - | Controlled open state. |
autoScrollOnOpen | boolean | false | Scrolls expanded content into view after opening. |
@open-change | (open: boolean) => void | - | Emitted when the collapsible state changes. |
@open-change-complete | (open: boolean) => void | - | Emitted once the content finishes showing or hiding, including sidebar expand/collapse and mobile drawer changes. The value reports content visibility. Never emitted during initial mount. |
@update:open | (open: boolean) => void | - | Controlled open update event. |
Sidebar.SlidingViews
Animated horizontal transitions between navigation surfaces.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
activeKey | string | - | Key of the currently active view. Must match a child SlidingView value. |
direction | "left" | "right" | "left" | Transition direction metadata. |
Accessibility
Landmark Semantics
Desktop sidebars render as aside; mobile sidebars render as a labeled navigation drawer with aria-hidden and inert while closed.
Keyboard Support
Trigger and close controls are native buttons. The resize handle supports arrow keys, Home, and End.
Hidden Views
Inactive sliding views and closed collapsible panels are marked aria-hidden and inert so they are not reachable by assistive technology.
Loading Feedback
Sidebar.Loading exposes role="status" with a configurable accessible label while keeping decorative skeleton blocks hidden from assistive technology.