Phi
Sidebar
@dicehub/phiv1.0.0-beta.1

Sidebar

A composable sidebar navigation component with collapsible groups, icon-only mode, peeking, sliding views, and responsive mobile support.

<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

The rendered sidebar container. Top-level state and layout props live on Sidebar.Provider.

Prop / EventTypeDefaultDescription
default slotslot-Header, Content, Footer, Rail, ResizeHandle, and other sidebar parts.
contentClass / contentClassNamestring | object | array-Additional classes applied to the desktop content container.
fullScreenOnMobilebooleanfalseExpands the mobile drawer to the full viewport while suppressing its backdrop and divider.
classstring | object | array-Additional classes applied to the sidebar rail.

Context provider managing expand/collapse state, mobile detection, peeking, and resizing.

Prop / EventTypeDefaultDescription
default slotslot-Typically contains Sidebar, page content, and mobile trigger controls.
defaultOpenbooleantrueInitial open state when uncontrolled.
openboolean-Controlled open state.
modelValue / v-modelboolean-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.
resizablebooleanfalseEnables drag-to-resize on the sidebar edge.
defaultWidthnumber256Initial width in pixels when resizable.
minWidthnumber200Minimum width in pixels when resizing.
maxWidthnumber480Maximum width in pixels when resizing.
containedbooleanfalseKeeps collapsed overlays and mobile drawer scoped inside a bounded parent.
peekablebooleanfalseHovering or focusing a collapsed sidebar temporarily expands it.
animationDurationnumber250Expand/collapse animation duration in milliseconds.
mobileBreakpointnumber768Viewport width below which the sidebar renders as a mobile drawer.
classstring | 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.

Scrollable middle section. Use Sidebar.Header and Sidebar.Footer to pin content above or below this area.

Prop / EventTypeDefaultDescription
default slotslot-Navigation groups, menus, and other scrollable sidebar content.
classstring | object | array-Additional classes applied to the content root.

Navigation-shaped loading state composed from SkeletonLine. Render it in place of Sidebar.Content while navigation data resolves.

Prop / EventTypeDefaultDescription
labelstring"Loading"Accessible label announced for the status region.
classstring | object | array-Additional classes forwarded to the loading-state root.

Primary interactive element. Supports icons, active state, links, and collapsed tooltips.

Prop / EventTypeDefaultDescription
itemIdstring-Unique item ID for scrollToItem and scrollItemIntoView. Also supported on Sidebar.MenuItem.
default slotslot-Visible label and optional trailing elements such as Sidebar.MenuChevron or Sidebar.MenuBadge.
iconComponent-Vue icon component rendered before the label.
activebooleanfalseMarks this item as active and sets data-active.
size"base" | "sm""base"Button size.
hrefstring-When set, renders as an anchor instead of a button.
targetstring-Anchor target when href is set.
aria-currentstring | boolean"page" when activeExplicit native link value is preserved; active links otherwise default to page.
tooltipstring-Native title tooltip shown when collapsed and not peeking.
disabledbooleanfalseDisables the button when rendered as button.
classstring | object | array-Additional classes applied to the button or link.

Button inside a sub-menu. Auto-wraps in li when not already inside Sidebar.MenuSubItem.

Prop / EventTypeDefaultDescription
default slotslot-Visible label and optional trailing elements.
activebooleanfalseMarks this sub-item as active and sets data-active.
hrefstring-When set, renders as an anchor instead of a button.
targetstring-Anchor target when href is set.
aria-currentstring | boolean"page" when activeExplicit native link value is preserved; active links otherwise default to page.
disabledbooleanfalseDisables the button when rendered as button.

Public compound parts exported on Sidebar for Vue slot composition.

Prop / EventTypeDefaultDescription
Sidebar.Headercomponent-Fixed header region above the scrollable navigation content.
Sidebar.Contentcomponent-Scrollable main navigation region.
Sidebar.Footercomponent-Fixed footer region below the scrollable navigation content.
Sidebar.Loadingcomponent-Status region with grouped navigation-row skeletons.
Sidebar.Groupcomponent-Groups a label and related menu items.
Sidebar.GroupLabelcomponent-Subtle group heading that hides in icon-only collapsed state.
Sidebar.Menucomponent-Menu list container.
Sidebar.MenuItemcomponent-Explicit li wrapper when a menu button should not auto-wrap.
Sidebar.MenuBadgecomponent-Trailing badge for counts or labels.
Sidebar.MenuSubcomponent-Indented nested menu list.
Sidebar.MenuSubItemcomponent-Explicit li wrapper for nested menu buttons.
Sidebar.Separatorcomponent-Visual separator between sidebar sections.
Sidebar.Triggercomponent-Button that toggles the sidebar and reflects aria-expanded.
Sidebar.Closecomponent-Ghost button that closes the mobile drawer. Intended for full-screen mobile headers.
Sidebar.Railcomponent-Invisible edge hit target that toggles collapsed sidebars.
Sidebar.ResizeHandlecomponent-Keyboard and pointer resize handle shown when Sidebar.Provider is resizable.
Sidebar.MenuChevroncomponent-Chevron that rotates from the nearest Sidebar.Collapsible state.
Sidebar.CollapsibleTriggercomponent-Clones its default slot and merges trigger attributes.
Sidebar.CollapsibleContentcomponent-Animated collapsible content panel.
Sidebar.SlidingViewcomponent-Individual panel inside Sidebar.SlidingViews; inactive panels are aria-hidden and inert.
useSidebarcomposable-Reads sidebar context, including scrollToItem(id, options) and scrollItemIntoView(id, options). Must be called inside Sidebar.Provider.

Collapsible wrapper for sidebar sub-menu expand/collapse.

Prop / EventTypeDefaultDescription
defaultOpenbooleanfalseInitial open state when uncontrolled.
openboolean-Controlled open state.
autoScrollOnOpenbooleanfalseScrolls 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.

Animated horizontal transitions between navigation surfaces.

Prop / EventTypeDefaultDescription
activeKeystring-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.