Sidebar
The wide navigation panel of an app shell. Sidebar is a bare frame — a fixed-width column with the collapse machinery and a single slot — and you compose the body from SidebarItem, SidebarLabel, and your own markup. The app owns its header, scroll region, and empty state; lay them out with plain flex utilities.
<script setup lang="ts">
import { ref } from 'vue'
import {
Sidebar,
SidebarItem,
SidebarLabel,
ScrollArea,
Button,
Dropdown,
} from 'frappe-ui'
// A faithful Gameplan sidebar: an app switcher up top, then a scrolling list of
// spaces with lucide icons, unread counts, private locks, and a hover-reveal
// options menu. Only <Sidebar>/<SidebarItem>/<SidebarLabel> come from the
// family — the header, the ScrollArea, and the spaces markup are the app's own.
const active = ref('product')
const sort = ref('Recent activity')
const spaces = [
{ id: 'product', title: 'Product', icon: 'lucide-rocket', unread: 0, private: false },
{ id: 'design', title: 'Design', icon: 'lucide-palette', unread: 3, private: false },
{ id: 'engineering', title: 'Engineering', icon: 'lucide-code', unread: 12, private: false },
{ id: 'marketing', title: 'Marketing', icon: 'lucide-megaphone', unread: 0, private: false },
{ id: 'sales', title: 'Sales', icon: 'lucide-trending-up', unread: 1, private: false },
{ id: 'support', title: 'Customer Support', icon: 'lucide-headphones', unread: 0, private: false },
{ id: 'people', title: 'People & Culture', icon: 'lucide-users', unread: 0, private: false },
{ id: 'finance', title: 'Finance', icon: 'lucide-wallet', unread: 0, private: true },
{ id: 'leadership', title: 'Leadership', icon: 'lucide-crown', unread: 2, private: true },
{ id: 'design-system', title: 'Design System', icon: 'lucide-component', unread: 0, private: false },
{ id: 'research', title: 'User Research', icon: 'lucide-microscope', unread: 5, private: false },
{ id: 'ops', title: 'Operations', icon: 'lucide-settings-2', unread: 0, private: false },
{ id: 'events', title: 'Events', icon: 'lucide-party-popper', unread: 0, private: false },
{ id: 'data', title: 'Data & Analytics', icon: 'lucide-database', unread: 8, private: false },
{ id: 'brand', title: 'Brand', icon: 'lucide-sparkles', unread: 0, private: false },
{ id: 'partnerships', title: 'Partnerships', icon: 'lucide-handshake', unread: 0, private: false },
{ id: 'security', title: 'Security', icon: 'lucide-shield', unread: 0, private: true },
{ id: 'onboarding', title: 'Onboarding', icon: 'lucide-graduation-cap', unread: 0, private: false },
{ id: 'random', title: 'Random', icon: 'lucide-shuffle', unread: 0, private: false },
]
const sortOptions = [
{
group: 'Sort by',
options: ['Recent activity', 'Alphabetical'].map((label) => ({
label,
icon: sort.value === label ? 'lucide-check' : null,
onClick: () => (sort.value = label),
})),
},
]
</script>
<template>
<div class="flex h-[560px] w-fit overflow-hidden rounded-md border">
<Sidebar disable-collapse width="14rem">
<!-- App switcher — the app owns the header. -->
<div class="flex shrink-0 items-center p-2">
<button
class="flex h-8 w-full items-center gap-2 rounded px-1 transition hover:bg-surface-gray-2"
>
<div
class="grid size-6 shrink-0 place-items-center rounded bg-surface-gray-7 text-xs font-medium text-ink-white"
>
F
</div>
<span class="flex-1 truncate text-left text-base text-ink-gray-8">Frappe</span>
<span class="lucide-chevrons-up-down size-4 shrink-0 text-ink-gray-5" />
</button>
</div>
<!--
The app owns the scroll region. frappe-ui's ScrollArea keeps the thin,
auto-hiding overlay scrollbar; padding the viewport (px-2) gives the
active row's shadow room so overflow-hidden doesn't clip it.
-->
<ScrollArea class="min-h-0 flex-1" viewport-class="px-2 pt-0.5 pb-10">
<div class="flex h-7 items-center justify-between">
<SidebarLabel>Spaces</SidebarLabel>
<div class="flex items-center">
<Dropdown :options="sortOptions" align="end">
<template #trigger="{ open }">
<Button
variant="ghost"
size="sm"
icon="lucide-arrow-up-down text-ink-gray-5"
label="Sort spaces"
tooltip="Sort spaces"
:active="open"
/>
</template>
</Dropdown>
<Button
variant="ghost"
size="sm"
icon="lucide-plus text-ink-gray-5"
label="New space"
/>
</div>
</div>
<nav class="mt-0.5 space-y-0.5">
<SidebarItem
v-for="space in spaces"
:key="space.id"
:icon="space.icon"
:active="active === space.id"
@click="active = space.id"
>
<span class="flex-1 inline-flex items-center gap-1 truncate text-sm">
<span
v-if="space.private"
class="lucide-lock size-3 shrink-0 text-ink-gray-5"
/>
{{ space.title }}
</span>
<template #suffix>
<!--
Count and options menu share one cell: the count fades out on row
hover/focus while the "…" menu fades in. The group is
SidebarItem's root (`group/sidebar-item`).
-->
<div class="relative mr-1 flex size-7 shrink-0 items-center justify-end">
<span
v-if="space.unread > 0"
class="absolute right-1 text-xs text-ink-gray-5 transition-opacity group-hover/sidebar-item:opacity-0 group-focus-within/sidebar-item:opacity-0"
>
{{ space.unread }}
</span>
<Dropdown
:options="[{ label: 'Mark all as read' }, { label: 'Leave space' }]"
align="start"
side="right"
>
<template #default="{ open }">
<Button
:variant="open ? 'subtle' : 'ghost'"
size="xs"
icon="lucide-more-horizontal text-ink-gray-5"
:label="`${space.title} options`"
class="absolute right-0 -mr-0.5 opacity-0 group-hover/sidebar-item:opacity-100 group-focus-within/sidebar-item:opacity-100"
:class="open ? 'opacity-100' : ''"
/>
</template>
</Dropdown>
</div>
</template>
</SidebarItem>
</nav>
</ScrollArea>
</Sidebar>
</div>
</template>There are no layout slots and no built-in scrolling in composition mode. Put a header as a direct child, wrap the middle list in your own overflow-y-auto container, and push a footer down with mt-auto.
Collapse
Sidebar owns collapse. Bind v-model:collapsed to control it, or leave it unset to collapse automatically below the sm breakpoint. disableCollapse pins it open. Width comes from the width / collapsedWidth props (CSS lengths, applied inline so an app can override them). Drop a SidebarCollapseToggle anywhere inside to flip the state; SidebarLabel divider turns a section label into a divider line while collapsed.
<script setup lang="ts">
import { ref } from 'vue'
import {
Sidebar,
SidebarItem,
SidebarLabel,
SidebarCollapseToggle,
} from 'frappe-ui'
const collapsed = ref(true)
const active = ref('inbox')
const items = [
{ id: 'inbox', label: 'Inbox', icon: 'lucide-inbox' },
{ id: 'starred', label: 'Starred', icon: 'lucide-star' },
{ id: 'sent', label: 'Sent', icon: 'lucide-send' },
]
</script>
<template>
<div class="flex h-[360px] w-fit overflow-hidden rounded-md border">
<!-- v-model:collapsed is app state; SidebarCollapseToggle flips it. -->
<Sidebar v-model:collapsed="collapsed">
<div class="flex-1 overflow-y-auto px-2 pt-2">
<SidebarLabel divider>Mail</SidebarLabel>
<SidebarItem
v-for="item in items"
:key="item.id"
:icon="item.icon"
:label="item.label"
:active="active === item.id"
@click="active = item.id"
/>
</div>
<div class="mt-auto px-2 pb-2">
<SidebarCollapseToggle />
</div>
</Sidebar>
</div>
</template>SidebarItem
A single row. It renders a container with a navigable main area and a sibling trailing zone, so an options menu in #suffix isn't nested inside the link (which anchors and buttons disallow).
#prefix— a leading icon or avatar (falls back to theiconprop: a lucide class, text, or a component).- default slot — the label region (falls back to the
labelprop). Put inline adornments like a lock icon here next to the text. #suffix— the trailing zone: an unread count, an options…menu, etc.
Set to to render a router link; omit it for a button. active drives data-state; when omitted it's inferred by matching to against the current route. A click invokes onClick (bound from @click) in both cases.
Legacy config API
Deprecated
The config-object API (:header and :sections="[{ items }]") still works for one release, reimplemented on top of the new sub-components. Prefer the composition API above for new code.
<script setup lang="ts">
// The deprecated config-object API, kept working for one release on top of the
// new sub-components. Prefer the composition API (see Default) for new code.
import { Sidebar } from 'frappe-ui'
const header = {
title: 'Frappe CRM',
subtitle: 'crm.frappe.io',
menuItems: [{ label: 'Settings' }, { label: 'Log out' }],
}
const sections = [
{
label: '',
items: [
{ label: 'Leads', to: '/leads', icon: 'lucide-user-plus' },
{ label: 'Deals', to: '/deals', icon: 'lucide-handshake' },
{ label: 'Contacts', to: '/contacts', icon: 'lucide-contact' },
],
},
{
label: 'Views',
collapsible: true,
items: [
{ label: 'My Open Deals', to: '/my-open-deals', icon: 'lucide-flame' },
{ label: 'Unassigned', to: '/unassigned', icon: 'lucide-circle-dashed' },
],
},
]
</script>
<template>
<div class="flex h-[420px] w-fit overflow-hidden rounded-md border">
<Sidebar :header="header" :sections="sections" disable-collapse />
</div>
</template>API Reference
Sidebar
Show types
import type {
Component,
ComputedRef,
InjectionKey,
MaybeRefOrGetter,
} from 'vue'
import { RouteLocationRaw } from 'vue-router'
/**
* Read-only collapsed state, provided by `Sidebar` and consumed by
* `SidebarItem` / `SidebarLabel` / `SidebarHeader` to shrink to icon-only.
*/
export const sidebarCollapsedKey: InjectionKey<ComputedRef<boolean>> =
Symbol('sidebarCollapsed')
/**
* Toggles the sidebar's collapsed state, provided by `Sidebar` and consumed by
* `SidebarCollapseToggle`. Kept separate from {@link sidebarCollapsedKey} so
* existing read-only consumers need no change.
*/
export const sidebarToggleKey: InjectionKey<() => void> =
Symbol('sidebarToggle')
export type SidebarProps = {
/**
* @deprecated Config-object header. Prefer composing your own header in the
* default slot. Kept for one release for backward compatibility.
*/
header?: SidebarHeaderProps
/**
* @deprecated Config-object sections. Prefer composing `SidebarLabel` +
* `SidebarItem` in the default slot. Kept for one release.
*/
sections?: SidebarSectionProps[]
/** Disables collapsing entirely (fixed width, no built-in toggle). */
disableCollapse?: boolean
/** Expanded width as a CSS length. Applied inline so apps can override it. */
width?: string
/** Collapsed width as a CSS length. */
collapsedWidth?: string
}
export interface SidebarItemProps {
/** Row label. Used as the accessible name and the default slot fallback. */
label?: string
/** `accesskey` attribute for a keyboard shortcut. */
accessKey?: string
/**
* Leading icon: a CSS class (e.g. `lucide-box`), plain text, or a component.
* Ignored when the `#prefix` slot is used.
*/
icon?: string | Component
/** Trailing text. Ignored when the `#suffix` slot is used. */
suffix?: string
/**
* Navigation target. When set the row's main area renders as a router link;
* otherwise it renders as a button. A click still invokes `onClick`.
*/
to?: RouteLocationRaw
/**
* Marks the row active (`data-state="active"`). When omitted, active state is
* inferred by matching `to` against the current route.
*/
active?: boolean
/** @deprecated Use `active`. Alias kept for the config-object path. */
isActive?: boolean
/**
* Click handler. Bound from `@click` in composition and from `item.onClick`
* in the config-object path — both resolve to this prop.
*/
onClick?: (event: MouseEvent) => void
/** @deprecated Config-object visibility flag; filtered by the legacy adapter. */
condition?: MaybeRefOrGetter<boolean>
}
export interface SidebarLabelProps {
/**
* When true, collapses to a horizontal divider while the sidebar is collapsed
* (matches the previous `SidebarSection` label behavior).
*/
divider?: boolean
}
export type SidebarHeaderProps = {
title: string
subtitle?: string
logo?: string | Component
/**
* Whether to render the leading logo/avatar box. Defaults to `true`. Set to
* `false` when workspace identity is already shown elsewhere (e.g. a left
* rail) to avoid a duplicate avatar; the title then sits flush-left. Best
* paired with a non-collapsing sidebar, since a collapsed header with no logo
* has nothing to show.
*/
showLogo?: boolean
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* @deprecated Legacy config-object section shape. Rendered by the internal
* `SidebarSection` adapter for `Sidebar`'s `sections` prop. New code composes
* `SidebarLabel` + `SidebarItem` directly.
*/
export type SidebarSectionProps = {
label?: string
items: SidebarItemProps[]
collapsible?: boolean
}Deprecated — Config-object header. Prefer composing your own header in the default slot. Kept for one release for backward compatibility.
Deprecated — Config-object sections. Prefer composing `SidebarLabel` + `SidebarItem` in the default slot. Kept for one release.
Disables collapsing entirely (fixed width, no built-in toggle).
Expanded width as a CSS length. Applied inline so apps can override it.
Collapsed width as a CSS length.
| Slot | Payload |
|---|---|
default | {} |
header-logo | {} |
footer-items | {} |
| Event | Payload |
|---|---|
update:collapsed | [value: boolean | null] Fired when the collapsed changes. |
Fired when the collapsed changes.
SidebarItem
Show types
import type {
Component,
ComputedRef,
InjectionKey,
MaybeRefOrGetter,
} from 'vue'
import { RouteLocationRaw } from 'vue-router'
/**
* Read-only collapsed state, provided by `Sidebar` and consumed by
* `SidebarItem` / `SidebarLabel` / `SidebarHeader` to shrink to icon-only.
*/
export const sidebarCollapsedKey: InjectionKey<ComputedRef<boolean>> =
Symbol('sidebarCollapsed')
/**
* Toggles the sidebar's collapsed state, provided by `Sidebar` and consumed by
* `SidebarCollapseToggle`. Kept separate from {@link sidebarCollapsedKey} so
* existing read-only consumers need no change.
*/
export const sidebarToggleKey: InjectionKey<() => void> =
Symbol('sidebarToggle')
export type SidebarProps = {
/**
* @deprecated Config-object header. Prefer composing your own header in the
* default slot. Kept for one release for backward compatibility.
*/
header?: SidebarHeaderProps
/**
* @deprecated Config-object sections. Prefer composing `SidebarLabel` +
* `SidebarItem` in the default slot. Kept for one release.
*/
sections?: SidebarSectionProps[]
/** Disables collapsing entirely (fixed width, no built-in toggle). */
disableCollapse?: boolean
/** Expanded width as a CSS length. Applied inline so apps can override it. */
width?: string
/** Collapsed width as a CSS length. */
collapsedWidth?: string
}
export interface SidebarItemProps {
/** Row label. Used as the accessible name and the default slot fallback. */
label?: string
/** `accesskey` attribute for a keyboard shortcut. */
accessKey?: string
/**
* Leading icon: a CSS class (e.g. `lucide-box`), plain text, or a component.
* Ignored when the `#prefix` slot is used.
*/
icon?: string | Component
/** Trailing text. Ignored when the `#suffix` slot is used. */
suffix?: string
/**
* Navigation target. When set the row's main area renders as a router link;
* otherwise it renders as a button. A click still invokes `onClick`.
*/
to?: RouteLocationRaw
/**
* Marks the row active (`data-state="active"`). When omitted, active state is
* inferred by matching `to` against the current route.
*/
active?: boolean
/** @deprecated Use `active`. Alias kept for the config-object path. */
isActive?: boolean
/**
* Click handler. Bound from `@click` in composition and from `item.onClick`
* in the config-object path — both resolve to this prop.
*/
onClick?: (event: MouseEvent) => void
/** @deprecated Config-object visibility flag; filtered by the legacy adapter. */
condition?: MaybeRefOrGetter<boolean>
}
export interface SidebarLabelProps {
/**
* When true, collapses to a horizontal divider while the sidebar is collapsed
* (matches the previous `SidebarSection` label behavior).
*/
divider?: boolean
}
export type SidebarHeaderProps = {
title: string
subtitle?: string
logo?: string | Component
/**
* Whether to render the leading logo/avatar box. Defaults to `true`. Set to
* `false` when workspace identity is already shown elsewhere (e.g. a left
* rail) to avoid a duplicate avatar; the title then sits flush-left. Best
* paired with a non-collapsing sidebar, since a collapsed header with no logo
* has nothing to show.
*/
showLogo?: boolean
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* @deprecated Legacy config-object section shape. Rendered by the internal
* `SidebarSection` adapter for `Sidebar`'s `sections` prop. New code composes
* `SidebarLabel` + `SidebarItem` directly.
*/
export type SidebarSectionProps = {
label?: string
items: SidebarItemProps[]
collapsible?: boolean
}Row label. Used as the accessible name and the default slot fallback.
`accesskey` attribute for a keyboard shortcut.
Leading icon: a CSS class (e.g. `lucide-box`), plain text, or a component. Ignored when the `#prefix` slot is used.
Trailing text. Ignored when the `#suffix` slot is used.
Navigation target. When set the row's main area renders as a router link; otherwise it renders as a button. A click still invokes `onClick`.
Marks the row active (`data-state="active"`). When omitted, active state is inferred by matching `to` against the current route.
Deprecated — Use `active`. Alias kept for the config-object path.
Click handler. Bound from `@click` in composition and from `item.onClick` in the config-object path — both resolve to this prop.
Deprecated — Config-object visibility flag; filtered by the legacy adapter.
| Slot | Payload |
|---|---|
prefix | {} |
default | {} |
suffix | {} |
SidebarLabel
Show types
import type {
Component,
ComputedRef,
InjectionKey,
MaybeRefOrGetter,
} from 'vue'
import { RouteLocationRaw } from 'vue-router'
/**
* Read-only collapsed state, provided by `Sidebar` and consumed by
* `SidebarItem` / `SidebarLabel` / `SidebarHeader` to shrink to icon-only.
*/
export const sidebarCollapsedKey: InjectionKey<ComputedRef<boolean>> =
Symbol('sidebarCollapsed')
/**
* Toggles the sidebar's collapsed state, provided by `Sidebar` and consumed by
* `SidebarCollapseToggle`. Kept separate from {@link sidebarCollapsedKey} so
* existing read-only consumers need no change.
*/
export const sidebarToggleKey: InjectionKey<() => void> =
Symbol('sidebarToggle')
export type SidebarProps = {
/**
* @deprecated Config-object header. Prefer composing your own header in the
* default slot. Kept for one release for backward compatibility.
*/
header?: SidebarHeaderProps
/**
* @deprecated Config-object sections. Prefer composing `SidebarLabel` +
* `SidebarItem` in the default slot. Kept for one release.
*/
sections?: SidebarSectionProps[]
/** Disables collapsing entirely (fixed width, no built-in toggle). */
disableCollapse?: boolean
/** Expanded width as a CSS length. Applied inline so apps can override it. */
width?: string
/** Collapsed width as a CSS length. */
collapsedWidth?: string
}
export interface SidebarItemProps {
/** Row label. Used as the accessible name and the default slot fallback. */
label?: string
/** `accesskey` attribute for a keyboard shortcut. */
accessKey?: string
/**
* Leading icon: a CSS class (e.g. `lucide-box`), plain text, or a component.
* Ignored when the `#prefix` slot is used.
*/
icon?: string | Component
/** Trailing text. Ignored when the `#suffix` slot is used. */
suffix?: string
/**
* Navigation target. When set the row's main area renders as a router link;
* otherwise it renders as a button. A click still invokes `onClick`.
*/
to?: RouteLocationRaw
/**
* Marks the row active (`data-state="active"`). When omitted, active state is
* inferred by matching `to` against the current route.
*/
active?: boolean
/** @deprecated Use `active`. Alias kept for the config-object path. */
isActive?: boolean
/**
* Click handler. Bound from `@click` in composition and from `item.onClick`
* in the config-object path — both resolve to this prop.
*/
onClick?: (event: MouseEvent) => void
/** @deprecated Config-object visibility flag; filtered by the legacy adapter. */
condition?: MaybeRefOrGetter<boolean>
}
export interface SidebarLabelProps {
/**
* When true, collapses to a horizontal divider while the sidebar is collapsed
* (matches the previous `SidebarSection` label behavior).
*/
divider?: boolean
}
export type SidebarHeaderProps = {
title: string
subtitle?: string
logo?: string | Component
/**
* Whether to render the leading logo/avatar box. Defaults to `true`. Set to
* `false` when workspace identity is already shown elsewhere (e.g. a left
* rail) to avoid a duplicate avatar; the title then sits flush-left. Best
* paired with a non-collapsing sidebar, since a collapsed header with no logo
* has nothing to show.
*/
showLogo?: boolean
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* @deprecated Legacy config-object section shape. Rendered by the internal
* `SidebarSection` adapter for `Sidebar`'s `sections` prop. New code composes
* `SidebarLabel` + `SidebarItem` directly.
*/
export type SidebarSectionProps = {
label?: string
items: SidebarItemProps[]
collapsible?: boolean
}When true, collapses to a horizontal divider while the sidebar is collapsed (matches the previous `SidebarSection` label behavior).
| Slot | Payload |
|---|---|
default | {} |
SidebarHeader
Show types
import type {
Component,
ComputedRef,
InjectionKey,
MaybeRefOrGetter,
} from 'vue'
import { RouteLocationRaw } from 'vue-router'
/**
* Read-only collapsed state, provided by `Sidebar` and consumed by
* `SidebarItem` / `SidebarLabel` / `SidebarHeader` to shrink to icon-only.
*/
export const sidebarCollapsedKey: InjectionKey<ComputedRef<boolean>> =
Symbol('sidebarCollapsed')
/**
* Toggles the sidebar's collapsed state, provided by `Sidebar` and consumed by
* `SidebarCollapseToggle`. Kept separate from {@link sidebarCollapsedKey} so
* existing read-only consumers need no change.
*/
export const sidebarToggleKey: InjectionKey<() => void> =
Symbol('sidebarToggle')
export type SidebarProps = {
/**
* @deprecated Config-object header. Prefer composing your own header in the
* default slot. Kept for one release for backward compatibility.
*/
header?: SidebarHeaderProps
/**
* @deprecated Config-object sections. Prefer composing `SidebarLabel` +
* `SidebarItem` in the default slot. Kept for one release.
*/
sections?: SidebarSectionProps[]
/** Disables collapsing entirely (fixed width, no built-in toggle). */
disableCollapse?: boolean
/** Expanded width as a CSS length. Applied inline so apps can override it. */
width?: string
/** Collapsed width as a CSS length. */
collapsedWidth?: string
}
export interface SidebarItemProps {
/** Row label. Used as the accessible name and the default slot fallback. */
label?: string
/** `accesskey` attribute for a keyboard shortcut. */
accessKey?: string
/**
* Leading icon: a CSS class (e.g. `lucide-box`), plain text, or a component.
* Ignored when the `#prefix` slot is used.
*/
icon?: string | Component
/** Trailing text. Ignored when the `#suffix` slot is used. */
suffix?: string
/**
* Navigation target. When set the row's main area renders as a router link;
* otherwise it renders as a button. A click still invokes `onClick`.
*/
to?: RouteLocationRaw
/**
* Marks the row active (`data-state="active"`). When omitted, active state is
* inferred by matching `to` against the current route.
*/
active?: boolean
/** @deprecated Use `active`. Alias kept for the config-object path. */
isActive?: boolean
/**
* Click handler. Bound from `@click` in composition and from `item.onClick`
* in the config-object path — both resolve to this prop.
*/
onClick?: (event: MouseEvent) => void
/** @deprecated Config-object visibility flag; filtered by the legacy adapter. */
condition?: MaybeRefOrGetter<boolean>
}
export interface SidebarLabelProps {
/**
* When true, collapses to a horizontal divider while the sidebar is collapsed
* (matches the previous `SidebarSection` label behavior).
*/
divider?: boolean
}
export type SidebarHeaderProps = {
title: string
subtitle?: string
logo?: string | Component
/**
* Whether to render the leading logo/avatar box. Defaults to `true`. Set to
* `false` when workspace identity is already shown elsewhere (e.g. a left
* rail) to avoid a duplicate avatar; the title then sits flush-left. Best
* paired with a non-collapsing sidebar, since a collapsed header with no logo
* has nothing to show.
*/
showLogo?: boolean
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* @deprecated Legacy config-object section shape. Rendered by the internal
* `SidebarSection` adapter for `Sidebar`'s `sections` prop. New code composes
* `SidebarLabel` + `SidebarItem` directly.
*/
export type SidebarSectionProps = {
label?: string
items: SidebarItemProps[]
collapsible?: boolean
}Whether to render the leading logo/avatar box. Defaults to `true`. Set to `false` when workspace identity is already shown elsewhere (e.g. a left rail) to avoid a duplicate avatar; the title then sits flush-left. Best paired with a non-collapsing sidebar, since a collapsed header with no logo has nothing to show.
| Slot | Payload |
|---|---|
logo | {} |
SidebarSection
Show types
import type {
Component,
ComputedRef,
InjectionKey,
MaybeRefOrGetter,
} from 'vue'
import { RouteLocationRaw } from 'vue-router'
/**
* Read-only collapsed state, provided by `Sidebar` and consumed by
* `SidebarItem` / `SidebarLabel` / `SidebarHeader` to shrink to icon-only.
*/
export const sidebarCollapsedKey: InjectionKey<ComputedRef<boolean>> =
Symbol('sidebarCollapsed')
/**
* Toggles the sidebar's collapsed state, provided by `Sidebar` and consumed by
* `SidebarCollapseToggle`. Kept separate from {@link sidebarCollapsedKey} so
* existing read-only consumers need no change.
*/
export const sidebarToggleKey: InjectionKey<() => void> =
Symbol('sidebarToggle')
export type SidebarProps = {
/**
* @deprecated Config-object header. Prefer composing your own header in the
* default slot. Kept for one release for backward compatibility.
*/
header?: SidebarHeaderProps
/**
* @deprecated Config-object sections. Prefer composing `SidebarLabel` +
* `SidebarItem` in the default slot. Kept for one release.
*/
sections?: SidebarSectionProps[]
/** Disables collapsing entirely (fixed width, no built-in toggle). */
disableCollapse?: boolean
/** Expanded width as a CSS length. Applied inline so apps can override it. */
width?: string
/** Collapsed width as a CSS length. */
collapsedWidth?: string
}
export interface SidebarItemProps {
/** Row label. Used as the accessible name and the default slot fallback. */
label?: string
/** `accesskey` attribute for a keyboard shortcut. */
accessKey?: string
/**
* Leading icon: a CSS class (e.g. `lucide-box`), plain text, or a component.
* Ignored when the `#prefix` slot is used.
*/
icon?: string | Component
/** Trailing text. Ignored when the `#suffix` slot is used. */
suffix?: string
/**
* Navigation target. When set the row's main area renders as a router link;
* otherwise it renders as a button. A click still invokes `onClick`.
*/
to?: RouteLocationRaw
/**
* Marks the row active (`data-state="active"`). When omitted, active state is
* inferred by matching `to` against the current route.
*/
active?: boolean
/** @deprecated Use `active`. Alias kept for the config-object path. */
isActive?: boolean
/**
* Click handler. Bound from `@click` in composition and from `item.onClick`
* in the config-object path — both resolve to this prop.
*/
onClick?: (event: MouseEvent) => void
/** @deprecated Config-object visibility flag; filtered by the legacy adapter. */
condition?: MaybeRefOrGetter<boolean>
}
export interface SidebarLabelProps {
/**
* When true, collapses to a horizontal divider while the sidebar is collapsed
* (matches the previous `SidebarSection` label behavior).
*/
divider?: boolean
}
export type SidebarHeaderProps = {
title: string
subtitle?: string
logo?: string | Component
/**
* Whether to render the leading logo/avatar box. Defaults to `true`. Set to
* `false` when workspace identity is already shown elsewhere (e.g. a left
* rail) to avoid a duplicate avatar; the title then sits flush-left. Best
* paired with a non-collapsing sidebar, since a collapsed header with no logo
* has nothing to show.
*/
showLogo?: boolean
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* @deprecated Legacy config-object section shape. Rendered by the internal
* `SidebarSection` adapter for `Sidebar`'s `sections` prop. New code composes
* `SidebarLabel` + `SidebarItem` directly.
*/
export type SidebarSectionProps = {
label?: string
items: SidebarItemProps[]
collapsible?: boolean
}| Slot | Payload |
|---|---|
sidebar-item | { item: SidebarItemProps; isCollapsed: boolean; } |