Accessible, unstyled collapsible application sidebar with rail and mobile-sheet modes.
|
|
| Package |
@vizejs/ui/sidebar |
| Maturity |
stable |
| Own the source |
vize lib pull sidebar |
| Requires |
context, controllable-state, dialog, id, primitive, shortcut |
| Aliases |
sidebar, app sidebar, context panel, navigation rail, off-canvas nav |
| Covers |
shadcn/ui Sidebar, HTML aside landmark, WAI-ARIA dialog pattern |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { SidebarProvider, Sidebar, SidebarRoot, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarRail, SidebarTrigger } from "@vizejs/ui/sidebar";
Or copy the source into your project with vize lib pull sidebar (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Application shell with a collapsible navigation sidebar, a rail, and a trigger in the main area. -->
<script setup lang="ts">
import { ref } from "vue";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from "@vizejs/ui/sidebar";
const open = ref(true);
</script>
<template>
<SidebarProvider v-model:open="open">
<Sidebar aria-label="Mail navigation">
<SidebarHeader>Acme Mail</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Mailboxes</SidebarGroupLabel>
<nav aria-label="Mailboxes">
<ul>
<li><a href="/mail/inbox" aria-current="page">Inbox</a></li>
<li><a href="/mail/drafts">Drafts</a></li>
<li><a href="/mail/sent">Sent</a></li>
<li><a href="/mail/archive">Archive</a></li>
</ul>
</nav>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>Signed in as [email protected]</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<SidebarTrigger />
<h1>Inbox</h1>
<p>The sidebar is {{ open ? "expanded" : "collapsed" }}.</p>
</SidebarInset>
</SidebarProvider>
</template>
API
Source: src/families/layout/sidebar/sidebar-provider.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
open |
boolean |
undefined |
Controlled desktop open state. undefined selects uncontrolled behavior. |
defaultOpen |
boolean |
true |
Initial desktop open state for uncontrolled use. |
openMobile |
boolean |
undefined |
Controlled mobile sheet open state. undefined selects uncontrolled behavior. |
defaultOpenMobile |
boolean |
false |
Initial mobile sheet open state for uncontrolled use. |
collapsible |
SidebarCollapsible |
"offcanvas" |
Desktop collapse mode: off-canvas, icon rail, or never. |
side |
SidebarSide |
"left" |
Screen edge the sidebar is attached to. |
variant |
SidebarVariant |
"sidebar" |
Consumer styling variant mirrored to data-variant. |
storage |
SidebarStorage |
undefined |
Adapter that persists the desktop open state. It is read only after mount, so server and hydration markup always start from open/defaultOpen. |
storageKey |
string |
"vize-sidebar-open" |
Key used with storage. |
keyboardShortcut |
string | null |
"Mod+B" |
Document-level shortcut that toggles the sidebar. null disables it. |
mobileQuery |
string | null |
"(max-width: 767px)" |
Media query that switches to the mobile sheet. Evaluated only on the client; server rendering assumes desktop. null disables the sheet. |
width |
string |
"16rem" |
Expanded width published as --vize-sidebar-width. |
widthIcon |
string |
"3rem" |
Rail width published as --vize-sidebar-width-icon. |
widthMobile |
string |
"18rem" |
Mobile sheet width published as --vize-sidebar-width-mobile. |
Events
| Event |
Payload |
Description |
update:open |
[value: boolean] |
Fired when the desktop open state requests a controlled value. |
update:openMobile |
[value: boolean] |
Fired when the mobile sheet requests a controlled value. |
open-change |
[value: boolean, previous: boolean, nativeEvent: Event | null] |
Fired after any distinct desktop open request. |
Slots
| Slot |
Slot props |
Description |
default |
SidebarSlotState |
Sidebar layout: SidebarRoot, SidebarInset, and triggers. |
Exposed
| Member |
Type |
Description |
open |
boolean |
Desktop open state. |
state |
SidebarState |
Desktop state token. |
isMobile |
boolean |
Whether the mobile query currently matches. Always false during SSR. |
openMobile |
boolean |
Whether the mobile sheet is open. |
collapsible |
SidebarCollapsible |
Desktop collapse mode. |
side |
SidebarSide |
Attached screen edge. |
variant |
SidebarVariant |
Styling variant. |
sidebarId |
string |
Id of the sidebar landmark, wired to trigger aria-controls. |
setOpen |
(value: boolean, event?: Event | null) => boolean |
Request a desktop open value and report whether it differs. |
setOpenMobile |
(value: boolean, event?: Event | null) => boolean |
Request a mobile sheet open value and report whether it differs. |
toggle |
(event?: Event | null) => boolean |
Toggle the mobile sheet on mobile, otherwise the desktop sidebar. |
Source: src/families/layout/sidebar/sidebar-root.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
"Sidebar" |
Accessible name of the complementary landmark and the mobile sheet. |
to |
string | HTMLElement |
"body" |
CSS selector or element the mobile sheet is moved into. |
portalDisabled |
boolean |
false |
Render the mobile sheet in place instead of teleporting it. |
Slots
| Slot |
Slot props |
Description |
default |
SidebarSlotState |
Sidebar contents. Receives the current Sidebar state. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
Source: src/families/layout/sidebar/sidebar-root.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
"Sidebar" |
Accessible name of the complementary landmark and the mobile sheet. |
to |
string | HTMLElement |
"body" |
CSS selector or element the mobile sheet is moved into. |
portalDisabled |
boolean |
false |
Render the mobile sheet in place instead of teleporting it. |
Slots
| Slot |
Slot props |
Description |
default |
SidebarSlotState |
Sidebar contents. Receives the current Sidebar state. |
Exposed
| Member |
Type |
Description |
element |
HTMLElement | null |
Rendered desktop landmark, or null while rendered as a mobile sheet. |
Source: src/families/layout/sidebar/sidebar-content.vue
Props
| Prop |
Type |
Default |
Description |
as |
PrimitiveAs |
"div" |
Element or component to render. |
Slots
| Slot |
Slot props |
Description |
default |
— |
Scrollable main area of the sidebar. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
Source: src/families/layout/sidebar/sidebar-footer.vue
Props
| Prop |
Type |
Default |
Description |
as |
PrimitiveAs |
"div" |
Element or component to render. |
Slots
| Slot |
Slot props |
Description |
default |
— |
Sticky bottom area of the sidebar. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
Source: src/families/layout/sidebar/sidebar-group.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned group id. null and undefined select a deterministic fallback. |
Slots
| Slot |
Slot props |
Description |
default |
— |
SidebarGroupLabel and group contents. |
Exposed
| Member |
Type |
Description |
element |
Element | null |
Rendered element or component instance. |
labelId |
string |
Id consumed by SidebarGroupLabel and wired to aria-labelledby. |
Source: src/families/layout/sidebar/sidebar-group-label.vue
Props
| Prop |
Type |
Default |
Description |
as |
PrimitiveAs |
"div" |
Element or component to render, for example a heading. |
Slots
| Slot |
Slot props |
Description |
default |
— |
Visible group label. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
Source: src/families/layout/sidebar/sidebar-header.vue
Props
| Prop |
Type |
Default |
Description |
as |
PrimitiveAs |
"div" |
Element or component to render. |
Slots
| Slot |
Slot props |
Description |
default |
— |
Sticky top area of the sidebar. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
Source: src/families/layout/sidebar/sidebar-inset.vue
Props
| Prop |
Type |
Default |
Description |
as |
PrimitiveAs |
"main" |
Element or component rendered for the main content beside the sidebar. |
Slots
| Slot |
Slot props |
Description |
default |
— |
Main application content. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
Source: src/families/layout/sidebar/sidebar-rail.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
"Toggle sidebar" |
Accessible name when no visible label supplies one. |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before toggling. Call preventDefault() to keep state unchanged. |
Slots
| Slot |
Slot props |
Description |
default |
SidebarSlotState |
Button contents. Receives the current Sidebar state. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
focus |
— |
|
Source: src/families/layout/sidebar/sidebar-trigger.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
"Toggle sidebar" |
Accessible name when no visible label supplies one. |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before toggling. Call preventDefault() to keep state unchanged. |
Slots
| Slot |
Slot props |
Description |
default |
SidebarSlotState |
Button contents. Receives the current Sidebar state. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
focus |
— |
|
Behavior
Normative behavior for the @vizejs/ui/sidebar compound primitive (application sidebar /
context panel). Every row is proven by the named mounted-DOM, SSR, or compile-time test.
| State x input |
Observable outcome |
Proven by |
| any desktop render |
Root is a labelled <aside> with a deterministic id, data-state, data-side, data-variant; groups are role="group" labelled by their label; inset is <main>; the provider publishes --vize-sidebar-width, --vize-sidebar-width-icon, --vize-sidebar-width-mobile. |
renders a labelled complementary landmark, sections, and CSS width variables |
collapsible="offcanvas", trigger click |
Toggles open; the trigger mirrors aria-expanded/aria-controls; the collapsed landmark gets inert and data-collapsible="offcanvas"; update:open and open-change report the request. |
trigger toggles an off-canvas sidebar and makes the collapsed landmark inert |
collapsible="icon", rail click |
Collapses to a rail that stays interactive (no inert); the rail is a tabindex="-1" button because the trigger is the keyboard path. |
icon collapse keeps the rail interactive and the rail toggles the sidebar |
collapsible="none" |
The sidebar is always expanded, desktop toggles are disabled, and nothing is emitted. |
collapsible none keeps the sidebar expanded and disables toggles |
controlled open; trigger click prevented |
Requests are emitted while state follows the prop; a prevented click does not toggle. |
controlled open waits for the parent and click is preventable |
storage adapter |
The persisted value is read only after mount and each desktop change is written back under storageKey. |
persisted state is restored after mount and written on change |
keyboardShortcut (default Mod+B) |
The document shortcut toggles the sidebar and consumes the key; null disables it. |
the Mod+B shortcut toggles the sidebar and can be disabled |
mobileQuery matches after mount |
The root renders as a modal Dialog sheet labelled like the landmark; the trigger drives openMobile (update:openMobile) and Escape closes it; leaving the query restores the <aside> and listeners are released on unmount. |
mobile query renders the sidebar as a modal sheet toggled by the trigger |
| provider expose |
open, openMobile, isMobile, state, side, variant, sidebarId, setOpen, setOpenMobile, and toggle are available. |
provider exposes typed state and programmatic controls |
| parts outside a provider |
Mounting throws VIZE_UI_CONTEXT_MISSING; a group label outside a group names the missing SidebarGroup provider. |
sidebar parts require a provider |
| SSR |
Isolated requests render byte-identical desktop markup (SSR assumes desktop and ignores storage), including inert for a collapsed off-canvas sidebar. |
renders byte-identical desktop markup across isolated SSR requests |
| hydration with storage |
Hydration reuses server nodes with zero warnings, then applies the persisted state. |
hydrates without mismatches before applying storage and the mobile query |
| public types |
Collapse mode, side, variant, and storage adapter are closed, typed contracts. |
src/families/layout/sidebar/sidebar.types.test-d.ts |
| DOM/SSR/Vapor |
Every part compiles in each renderer lane. |
scripts/check-renderers.ts |
Components
| Component |
Contract |
sidebar-provider.vue |
Owns desktop and mobile state, persistence, the shortcut, the mobile query, and width variables. |
sidebar-root.vue |
Renders the <aside> landmark on desktop or a modal Dialog sheet on mobile. |
sidebar-trigger.vue |
Native button that toggles the sidebar (mobile sheet on mobile) with aria-expanded/aria-controls. |
sidebar-rail.vue |
Pointer hit area on the sidebar edge that toggles it; removed from the tab order. |
sidebar-header.vue |
Header section with data-state. |
sidebar-content.vue |
Scrollable content section with data-state. |
sidebar-footer.vue |
Footer section with data-state. |
sidebar-group.vue |
role="group" wrapper labelled by its SidebarGroupLabel. |
sidebar-group-label.vue |
Label whose id labels the enclosing group. |
sidebar-inset.vue |
Main content region (<main> by default) beside the sidebar. |
Sidebar ships no stylesheet. Consumers style data-state, data-collapsible, data-side,
data-variant, data-mobile, and the width variables.