MazSidebar
MazSidebar is a composable sidebar system for dashboard, admin, and SaaS applications. It provides a fully accessible, collapsible navigation with multiple display modes.
Before use it, you should install the MazUi plugin in your project, follow instructions in Getting Started
TIP
MazSidebar shares its state with all sub-components via Vue's provide/inject. No separate Provider component is needed. All sub-components must be descendants of MazSidebar.
Deferred features (v1.1)
The following features are planned for a future release and are not yet available:
localStoragestate persistenceCmd/Ctrl+Bkeyboard shortcut- Automatic mobile drawer under a breakpoint
Sub-components
| Component | Role |
|---|---|
MazSidebar | Root container — provides state context |
MazSidebarHeader | Fixed top area |
MazSidebarContent | Scrollable central area |
MazSidebarFooter | Fixed bottom area |
MazSidebarGroup | Item grouping with optional label |
MazSidebarSeparator | Visual divider (<hr>) |
MazSidebarMenu | <ul> wrapper with role="menu" |
MazSidebarMenuItem | <li> item wrapper |
MazSidebarMenuButton | Clickable item (link or button), supports icon, label, badge, tooltip |
MazSidebarMenuSub | Collapsible sub-menu |
MazSidebarTrigger | Toggle button (must be a descendant of MazSidebar) |
Basic usage
<div class="maz:flex maz:h-screen">
<MazSidebar v-model:open="isOpen">
<MazSidebarHeader>
<div class="maz:p-4 maz:font-bold">My App</div>
</MazSidebarHeader>
<MazSidebarContent>
<MazSidebarMenu>
<MazSidebarMenuItem>
<MazSidebarMenuButton label="Dashboard" :active="true" />
</MazSidebarMenuItem>
<MazSidebarMenuItem>
<MazSidebarMenuButton label="Settings" />
</MazSidebarMenuItem>
</MazSidebarMenu>
</MazSidebarContent>
<MazSidebarFooter>
<div class="maz:p-4">Footer</div>
</MazSidebarFooter>
</MazSidebar>
<main class="maz:flex-1 maz:p-4">Main content</main>
</div>
<script setup>
const isOpen = ref(true)
</script>Groups & dividers
<MazSidebar v-model:open="isOpen">
<MazSidebarContent>
<MazSidebarGroup label="Navigation">
<MazSidebarMenu>
<MazSidebarMenuItem>
<MazSidebarMenuButton label="Dashboard" :active="true" />
</MazSidebarMenuItem>
<MazSidebarMenuItem>
<MazSidebarMenuButton label="Projects" />
</MazSidebarMenuItem>
</MazSidebarMenu>
</MazSidebarGroup>
<MazSidebarSeparator />
<MazSidebarGroup label="Settings">
<MazSidebarMenu>
<MazSidebarMenuItem>
<MazSidebarMenuButton label="Profile" />
</MazSidebarMenuItem>
</MazSidebarMenu>
</MazSidebarGroup>
</MazSidebarContent>
</MazSidebar>Sub-menus
<MazSidebar v-model:open="isOpen">
<MazSidebarContent>
<MazSidebarMenu>
<MazSidebarMenuItem>
<MazSidebarMenuSub label="Products" :default-open="true">
<MazSidebarMenuItem>
<MazSidebarMenuButton label="All Products" />
</MazSidebarMenuItem>
<MazSidebarMenuItem>
<MazSidebarMenuButton label="Add Product" />
</MazSidebarMenuItem>
</MazSidebarMenuSub>
</MazSidebarMenuItem>
</MazSidebarMenu>
</MazSidebarContent>
</MazSidebar>Icon mode with tooltips
When collapsible="icon" and the sidebar is collapsed, labels fade out and only the icons stay visible. To help users identify items in collapsed state, provide a tooltip prop on each MazSidebarMenuButton: it uses the v-tooltip directive and is only shown on hover when the sidebar is collapsed by default.
Controlling when the tooltip appears
Both MazSidebar and MazSidebarMenuButton accept a tooltip-mode prop:
closed(default on the sidebar) — tooltip only shows on hover when the sidebar is collapsedalways— tooltip shows on hover regardless of the sidebar state
Set it on the sidebar to apply to every descendant button, or override per-button:
<!-- Every button: tooltip visible only when collapsed -->
<MazSidebar tooltip-mode="closed">
<MazSidebarMenuButton :icon="MazHome" label="Dashboard" tooltip="Go to Dashboard" />
</MazSidebar>
<!-- This button overrides: tooltip always shown -->
<MazSidebar tooltip-mode="closed">
<MazSidebarMenuButton
:icon="MazCog6Tooth"
label="Settings"
tooltip="Open Settings"
tooltip-mode="always"
/>
</MazSidebar>Performance
When no tooltip prop is provided on a button, the v-tooltip directive is not attached at all — no listeners, no popover instance.
<script setup>
import { MazHome } from '@maz-ui/icons/lazy/MazHome'
import { MazCog6Tooth } from '@maz-ui/icons/lazy/MazCog6Tooth'
const isOpen = ref(true)
</script>
<template>
<MazSidebar v-model:open="isOpen" collapsible="icon">
<MazSidebarContent>
<MazSidebarMenu>
<MazSidebarMenuItem>
<MazSidebarMenuButton :icon="MazHome" label="Dashboard" tooltip="Go to Dashboard" />
</MazSidebarMenuItem>
</MazSidebarMenu>
</MazSidebarContent>
<MazSidebarFooter>
<MazSidebarTrigger />
</MazSidebarFooter>
</MazSidebar>
</template>Hover mode
collapsible="hover" displays the sidebar in icon-only mode and automatically expands it when the mouse enters the sidebar (or any descendant) and collapses it when the mouse leaves. Keyboard focus also expands the sidebar, so users navigating with Tab see the full labels.
TIP
This is a purely visual state. The v-model:open value is not updated and the persistence cookie is not written when the sidebar expands or collapses through hover. Use this mode when you want the rail-style sidebar of collapsible="icon" without exposing a toggle.
<MazSidebar collapsible="hover">
<MazSidebarContent>
<MazSidebarMenu>
<MazSidebarMenuItem>
<MazSidebarMenuButton :icon="MazHome" label="Dashboard" />
</MazSidebarMenuItem>
<MazSidebarMenuItem>
<MazSidebarMenuButton :icon="MazCog6Tooth" label="Settings" />
</MazSidebarMenuItem>
</MazSidebarMenu>
</MazSidebarContent>
</MazSidebar>Offcanvas mode
<MazSidebar v-model:open="isOpen" collapsible="offcanvas">
<!-- content -->
</MazSidebar>Overlay vs push mode
In mode="push" (default), the sidebar is part of the document flow and pushes the main content. In mode="overlay", the sidebar floats over the content with a backdrop, escape key support and focus trapping.
<!-- Overlay mode: sidebar floats over content -->
<MazSidebar v-model:open="isOpen" mode="overlay">
<!-- content -->
</MazSidebar>Side end (right)
<div class="maz:flex maz:h-screen">
<main class="maz:flex-1">Main content</main>
<MazSidebar v-model:open="isOpen" side="end">
<!-- content -->
</MazSidebar>
</div>Badges
badge accepts a string, a number, or a full MazBadgeProps object (with an optional text) to fine-tune color, size, outlined, etc.
<!-- Simple: pass a string or number -->
<MazSidebarMenuButton label="Inbox" :badge="12" />
<!-- Full customisation: pass a MazBadgeProps object -->
<MazSidebarMenuButton
label="Errors"
:badge="{ text: 3, color: 'destructive' }"
/>
<MazSidebarMenuButton
label="Beta"
:badge="{ text: 'NEW', color: 'success', outlined: true }"
/>User menu in footer
<MazSidebar v-model:open="isOpen">
<MazSidebarContent><!-- ... --></MazSidebarContent>
<MazSidebarFooter>
<div class="maz:flex maz:items-center maz:gap-3 maz:p-3">
<MazAvatar src="..." size="0.8rem" />
<div class="maz:flex-1 maz:min-w-0">
<p class="maz:m-0 maz:text-md maz:font-semibold">Jane Doe</p>
<p class="maz:m-0 maz:text-xs maz:text-muted">jane@example.com</p>
</div>
</div>
</MazSidebarFooter>
</MazSidebar>Mobile (static rendering)
INFO
Automatic mobile drawer (drawer auto under a breakpoint with backdrop) is a deferred v1.1 feature. For now, control the sidebar visibility programmatically based on viewport.
<script setup>
import { useWindowSize } from 'maz-ui'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 768)
const isOpen = ref(!isMobile.value)
watch(isMobile, (mobile) => {
isOpen.value = !mobile
})
</script>
<template>
<div class="maz:flex maz:h-screen">
<MazSidebar v-model:open="isOpen" :mode="isMobile ? 'overlay' : 'push'">
<!-- content -->
</MazSidebar>
<main class="maz:flex-1">
<MazSidebarTrigger />
<!-- page content -->
</main>
</div>
</template>Programmatic control (v-model + composable)
Use v-model:open for two-way binding, or useSidebar() inside any descendant component.
<script setup>
import { useSidebar } from 'maz-ui'
// Inside a child component that is a descendant of MazSidebar:
const sidebar = useSidebar()
// sidebar.open.value → boolean
// sidebar.state.value → 'expanded' | 'collapsed'
// sidebar.toggle() → toggle open state
// sidebar.setOpen(true/false) → set explicitly
</script>
<template>
<MazSidebar v-model:open="isOpen">
<MazSidebarContent>
<MazSidebarMenu>
<MazSidebarMenuItem>
<MazSidebarMenuButton label="Dashboard" />
</MazSidebarMenuItem>
</MazSidebarMenu>
</MazSidebarContent>
<!-- MazSidebarTrigger uses useSidebar() internally -->
<MazSidebarTrigger />
</MazSidebar>
</template>Persistent open state
The sidebar persists its open/collapsed state in a cookie (maz-sidebar-open) so it survives page reloads. This is enabled by default.
<!-- Default: persistence enabled, cookie name "maz-sidebar-open" -->
<MazSidebar v-model:open="isOpen" />
<!-- Disable persistence -->
<MazSidebar v-model:open="isOpen" :persist="false" />
<!-- Custom cookie key (useful when multiple sidebars coexist) -->
<MazSidebar v-model:open="isOpen" persist-key="admin-sidebar-open" />How it works
- In setup (server + client): the cookie is read and used to initialize the open state. On the server, the value is taken from the request
Cookieheader via Vue'suseSSRContext(); on the client it comes fromdocument.cookie. - On client mount:
update:openis emitted if the persisted value differs from the:openprop, so your parentv-modelstays in sync. - On state change: every transition between expanded/collapsed writes the new value to the cookie (1-year expiry,
SameSite=Lax).
SSR considerations
Persistence is SSR-native — the server reads the request cookie and renders the sidebar in its persisted state immediately, so there is no expand → collapse flash on hydration. This works out of the box in Nuxt and any Vue 3 SSR runtime that exposes the request headers through useSSRContext().
If you prefer to manage the state yourself (e.g. to share it with other parts of your app), disable persistence and feed the cookie via your own ref:
<script setup>
const sidebarOpen = useCookie<boolean>('maz-sidebar-open', { default: () => true })
</script>
<template>
<MazSidebar v-model:open="sidebarOpen" :persist="false">
<!-- ... -->
</MazSidebar>
</template>:persist="false" prevents the sidebar from also writing the cookie, leaving full control to your useCookie ref.
With router (vue-router / NuxtLink)
MazSidebarMenuButton uses resolveLinkComponent() internally — it detects RouterLink (vue-router) or a provided NuxtLink. Pass the to prop for router navigation, href for plain anchor links.
<!-- With vue-router -->
<MazSidebarMenuButton :to="{ name: 'home' }" label="Home" />
<MazSidebarMenuButton to="/about" label="About" />
<!-- With plain href -->
<MazSidebarMenuButton href="https://maz-ui.com" label="Docs" />
<!-- Mark as active manually (useful when router is not available) -->
<MazSidebarMenuButton label="Dashboard" :active="$route.name === 'dashboard'" />CSS Variables
Customize the sidebar width using these CSS custom properties (also settable via width / iconWidth props):
:root {
--maz-sidebar-width: 16rem; /* expanded width */
--maz-sidebar-icon-width: 3rem; /* icon-mode collapsed width */
}Accessibility
- The sidebar root element renders as
<aside aria-label="Sidebar"> MazSidebarMenurenders as<ul role="menu">MazSidebarMenuItemrenders as<li role="none">MazSidebarTriggerhasaria-expandedandaria-controlsreferencing the sidebaridMazSidebarMenuButtonappliesaria-current="page"on active items and exposes the label viaaria-labelMazSidebarMenuSubtrigger hasaria-expandedandaria-controls- In
mode="overlay", pressingEscapecloses the sidebar and focus is restored to the trigger element - In
mode="overlay",Tab/Shift+Tabare trapped within the sidebar - All transitions respect
prefers-reduced-motion
MazSidebar
Props
| Name | Description | Type | Required | Default |
|---|---|---|---|---|
| id | Unique identifier for the sidebar (auto-generated if not provided) | string | No | undefined |
| open | Whether the sidebar is open | boolean | No | undefined |
| side | Side of the sidebar | "start" | "end" | No | undefined |
| collapsible | How the sidebar collapses - offcanvas: completely hidden - icon: only icons visible - hover: only icons visible by default, expands on mouse hover or keyboard focus (transient visual state — does not update v-model:open nor the persistence cookie) - none: always fully visible | "offcanvas" | "icon" | "hover" | "none" | No | undefined |
| mode | How the sidebar affects layout - push: sidebar is in document flow - overlay: sidebar floats over content with a backdrop | "push" | "overlay" | No | undefined |
| width | Width of the expanded sidebar | string | No | undefined |
| icon-width | Width of the collapsed sidebar in icon mode | string | No | undefined |
| tooltip-mode | When descendant MazSidebarMenuButton components should display their tooltip. - always: tooltip shows on hover regardless of the sidebar state - closed: tooltip only shows on hover when the sidebar is collapsed Individual buttons can override this via their own tooltipMode prop. | MazSidebarTooltipMode | No | closed |
| persist | Persist the open/collapsed state in a cookie so it survives reloads. Restoration happens on client mount (SSR-safe). For zero-flash SSR, read the cookie server-side and forward it via v-model:open. | boolean | No | true |
| persist-key | Cookie key used when persist is enabled. Set a unique key per sidebar instance if you mount several sidebars on the same site. | string | No | maz-sidebar-open |
Events
| Event name | Properties | Description |
|---|---|---|
| update:open | Emitted when open state changes |
Slots
| Name | Description | Bindings |
|---|---|---|
| default |
Expose
toggle
setOpen
MazSidebarMenuButton
Props
| Name | Description | Type | Required |
|---|---|---|---|
| to | Route for router-link navigation | string | Record | No |
| href | URL for anchor navigation | string | No |
| icon | Icon to display before the label. Accepts a bare icon value (Vue component, raw SVG string, URL) or a full MazIconProps object for fine-grained control (size, title, fallback, …). | MazIconLike | No |
| label | Label text | string | No |
| badge | Badge content. Pass a string/number for a simple primary badge, or a full MazBadgeProps object (with optional text) to customise color, size, outlined, … | MazSidebarMenuButtonBadge | No |
| tooltip | Tooltip text. When provided, shows a tooltip on hover via the v-tooltip directive. | string | No |
| tooltip-mode | Controls when the tooltip should be displayed. - always: tooltip shows on hover regardless of the sidebar state - closed: tooltip only shows on hover when the sidebar is collapsed Overrides the parent MazSidebar's tooltipMode. Falls back to the sidebar value (which defaults to always) when not provided. | MazSidebarTooltipMode | No |
| active | Whether the item is active (highlights with aria-current="page") | boolean | No |
| disabled | Whether the item is disabled | boolean | No |
| size | Size of the button | "sm" | "md" | "lg" | No |
Events
| Event name | Properties | Description |
|---|---|---|
| click |
Slots
| Name | Description | Bindings |
|---|---|---|
| icon | ||
| default |
MazSidebarMenuSub
Props
| Name | Description | Type | Required |
|---|---|---|---|
| label | Label for the sub-menu trigger button | string | No |
| icon | Icon to display before the label. Accepts a bare icon value (Vue component, raw SVG string, URL) or a full MazIconProps object for fine-grained control. | MazIconLike | No |
| default-open | Whether the sub-menu is open by default | boolean | No |
Slots
| Name | Description | Bindings |
|---|---|---|
| icon | ||
| label | ||
| chevron | ||
| default |
MazSidebarGroup
Props
| Name | Description | Type | Required |
|---|---|---|---|
| label | Optional label for the group | string | No |
Slots
| Name | Description | Bindings |
|---|---|---|
| default |