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-5 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-4 px-1 transition hover:bg-surface-gray-2"
>
<div
class="grid size-6 shrink-0 place-items-center rounded-4 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-5 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.
SidebarHeader
The app-switcher / workspace-identity row. A fixed 48px region that lines up with PageHeader, rendered as a dropdown trigger. title and subtitle are plain strings; #prefix fills the default logo/initial box (a size-7 overflow-hidden frame — wide content clips), falling back to the logo prop, or the title's first letter; showLogo: false drops the box entirely for a flush-left title. menuItems renders inside the trigger's dropdown — the same structured-options shape Dropdown itself takes.
SidebarSection
A collapsible group. It owns only the label row and the collapse chrome — compose SidebarItem (or anything else) as children in the default slot. Non-collapsible groups don't need this component at all: compose SidebarLabel
SidebarItemdirectly instead.
<script setup lang="ts">
import { ref } from 'vue'
import { Sidebar, SidebarSection, SidebarItem, SidebarLabel } from 'frappe-ui'
// Non-collapsible groups skip SidebarSection entirely: SidebarLabel +
// SidebarItem, composed directly. SidebarSection is only for groups that
// collapse — `viewsCollapsed` is app state its `v-model:collapsed` writes
// back to, so the app can persist the choice.
const active = ref('leads')
const viewsCollapsed = ref(false)
</script>
<template>
<div class="flex h-[360px] w-fit overflow-hidden rounded-5 border">
<Sidebar disable-collapse width="14rem">
<div class="flex-1 overflow-y-auto px-2 pt-2">
<SidebarLabel>Pipeline</SidebarLabel>
<SidebarItem
label="Leads"
icon="lucide-user-plus"
:active="active === 'leads'"
@click="active = 'leads'"
/>
<SidebarItem
label="Deals"
icon="lucide-handshake"
:active="active === 'deals'"
@click="active = 'deals'"
/>
<SidebarSection
label="Views"
collapsible
v-model:collapsed="viewsCollapsed"
>
<SidebarItem
label="My Open Deals"
icon="lucide-flame"
:active="active === 'my-open-deals'"
@click="active = 'my-open-deals'"
/>
<SidebarItem
label="Unassigned"
icon="lucide-circle-dashed"
:active="active === 'unassigned'"
@click="active = 'unassigned'"
/>
</SidebarSection>
</div>
</Sidebar>
</div>
</template>Bind v-model:collapsed to own a section's state (start a section collapsed, persist the choice); left unbound the section manages it internally, starting expanded.
SidebarCard
A promotional or onboarding card for the sidebar footer — a trial notice, an upgrade prompt, a "what's new" pointer. A white card with an optional theme-colored icon and one full-width tinted action button. Like Alert, it is stateless: dismiss is an event and the parent owns hiding the card. It is not a status announcement, so it has no live-region role.
<script setup lang="ts">
import { ref } from 'vue'
import { SidebarCard } from 'frappe-ui'
// Cards as they sit in a sidebar footer: on the sidebar's gray surface,
// at sidebar width. The parent owns hiding — dismiss just flips a flag.
// Actions report what they did in the status line below the grid.
const showFeatureCard = ref(true)
const status = ref('')
</script>
<template>
<div class="grid w-full max-w-lg grid-cols-2 items-start gap-4">
<div class="rounded-5 bg-surface-gray-1 p-3">
<SidebarCard
title="Your trial ends soon!"
description="Upgrade to keep enjoying features."
:action="{
label: 'Update now',
onClick: () => {
status = 'Billing page opened'
},
}"
/>
</div>
<div class="rounded-5 bg-surface-gray-1 p-3">
<SidebarCard
v-if="showFeatureCard"
theme="blue"
dismissible
title="New feature available"
description="Discover the new board view for your deals."
:action="{
label: 'Explore now',
onClick: ({ dismiss }) => ((status = 'Board view opened'), dismiss()),
}"
@dismiss="showFeatureCard = false"
/>
<button
v-else
class="text-sm text-ink-gray-5 underline"
@click="showFeatureCard = true"
>
Bring the card back
</button>
</div>
<div class="rounded-5 bg-surface-gray-1 p-3">
<SidebarCard
theme="amber"
title="Storage is almost full"
description="Free up space or upgrade your plan."
:action="{
label: 'Manage storage',
onClick: () => {
status = 'Storage settings opened'
},
}"
/>
</div>
<div class="rounded-5 bg-surface-gray-1 p-3">
<SidebarCard
theme="red"
title="Payment failed"
description="Update your card to avoid interruption."
:action="{
label: 'Fix billing',
onClick: () => {
status = 'Card details opened'
},
}"
/>
</div>
<p v-if="status" class="col-span-2 text-sm text-ink-gray-5">
{{ status }}
</p>
</div>
</template>action takes ButtonProps plus an onClick({ dismiss }) handler (the same shape as Alert's actions). #prefix, #title, #description, and #actions override the corresponding parts.
API Reference
Sidebar
Show types
import type { Component, ComputedRef, InjectionKey } from 'vue'
import { RouteLocationRaw } from 'vue-router'
import type { AlertAction } from '../Alert'
import type { StatusTheme } from '../shared/statusIcon'
/**
* 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 = {
/** 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
/** Click handler. Bound from `@click`. */
onClick?: (event: MouseEvent) => void
}
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 = {
/** Workspace or app title. */
title: string
/** Secondary line under the title, e.g. a domain or workspace slug. */
subtitle?: string
/** Leading logo: an image URL, or a component. Overridden by the `#prefix` slot. */
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
/** Options rendered in the trigger's dropdown — the same shape `Dropdown` itself takes. */
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* Promotional or onboarding card for the sidebar footer — a trial notice, an
* upgrade prompt, a "what's new" pointer. Stateless: `dismiss` is an event and
* the parent owns hiding the card.
*/
export interface SidebarCardProps {
/** Main heading text of the card. Optional when the `#title` slot is used */
title?: string
/** Supporting text below the title */
description?: string
/** Color theme of the icon and the tinted action button; the white container never changes with theme */
theme?: StatusTheme
/** Icon next to the title: unset shows the theme's auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph */
icon?: boolean | string | Component
/** The full-width tinted action button (`ButtonProps` plus `onClick({ dismiss })`) */
action?: AlertAction
/** Shows the dismiss (×) button, which emits `dismiss` */
dismissible?: boolean
}
export interface SidebarCardEmits {
/** Fired when the user dismisses the card — the × button or the action's `context.dismiss()`. The parent owns hiding. */
dismiss: []
}
/** Scoped payload for the card's `#actions` slot. */
export interface SidebarCardActionsSlotProps {
/** Emits the card's `dismiss` event. */
dismiss: () => void
}
export interface SidebarCardSlots {
/** Overrides the icon area next to the title */
prefix?: () => any
/** Rich title content (overrides the `title` prop) */
title?: () => any
/** Rich description content (overrides the `description` prop) */
description?: () => any
/** Replaces the auto-rendered action button; receives `{ dismiss }` */
actions?: (props: SidebarCardActionsSlotProps) => any
}
/**
* Building block for a collapsible group of `SidebarItem`s. Compose children
* in the default slot — `SidebarSection` owns only the label + collapse
* chrome, not the rows inside it.
*/
export type SidebarSectionProps = {
/** Section label. Renders nothing when omitted (a label-less group). */
label?: string
/** Whether clicking the label toggles the group's visibility. */
collapsible?: boolean
}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.
v-model. Whether the sidebar is collapsed. Left unset, it collapses automatically below the `sm` breakpoint.
| Slot | Payload |
|---|---|
default | — The sidebar body — header, scroll region, footer, all composed by the app. |
The sidebar body — header, scroll region, footer, all composed by the app.
| Event | Payload |
|---|---|
update:collapsed | [value: boolean | null] Fired when the sidebar is collapsed or expanded. |
Fired when the sidebar is collapsed or expanded.
SidebarItem
Show types
import type { Component, ComputedRef, InjectionKey } from 'vue'
import { RouteLocationRaw } from 'vue-router'
import type { AlertAction } from '../Alert'
import type { StatusTheme } from '../shared/statusIcon'
/**
* 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 = {
/** 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
/** Click handler. Bound from `@click`. */
onClick?: (event: MouseEvent) => void
}
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 = {
/** Workspace or app title. */
title: string
/** Secondary line under the title, e.g. a domain or workspace slug. */
subtitle?: string
/** Leading logo: an image URL, or a component. Overridden by the `#prefix` slot. */
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
/** Options rendered in the trigger's dropdown — the same shape `Dropdown` itself takes. */
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* Promotional or onboarding card for the sidebar footer — a trial notice, an
* upgrade prompt, a "what's new" pointer. Stateless: `dismiss` is an event and
* the parent owns hiding the card.
*/
export interface SidebarCardProps {
/** Main heading text of the card. Optional when the `#title` slot is used */
title?: string
/** Supporting text below the title */
description?: string
/** Color theme of the icon and the tinted action button; the white container never changes with theme */
theme?: StatusTheme
/** Icon next to the title: unset shows the theme's auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph */
icon?: boolean | string | Component
/** The full-width tinted action button (`ButtonProps` plus `onClick({ dismiss })`) */
action?: AlertAction
/** Shows the dismiss (×) button, which emits `dismiss` */
dismissible?: boolean
}
export interface SidebarCardEmits {
/** Fired when the user dismisses the card — the × button or the action's `context.dismiss()`. The parent owns hiding. */
dismiss: []
}
/** Scoped payload for the card's `#actions` slot. */
export interface SidebarCardActionsSlotProps {
/** Emits the card's `dismiss` event. */
dismiss: () => void
}
export interface SidebarCardSlots {
/** Overrides the icon area next to the title */
prefix?: () => any
/** Rich title content (overrides the `title` prop) */
title?: () => any
/** Rich description content (overrides the `description` prop) */
description?: () => any
/** Replaces the auto-rendered action button; receives `{ dismiss }` */
actions?: (props: SidebarCardActionsSlotProps) => any
}
/**
* Building block for a collapsible group of `SidebarItem`s. Compose children
* in the default slot — `SidebarSection` owns only the label + collapse
* chrome, not the rows inside it.
*/
export type SidebarSectionProps = {
/** Section label. Renders nothing when omitted (a label-less group). */
label?: string
/** Whether clicking the label toggles the group's visibility. */
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.
Click handler. Bound from `@click`.
| Slot | Payload |
|---|---|
prefix | — Leading icon or avatar. Overrides the `icon` prop. |
default | — The label region. Overrides the `label` prop; put inline adornments here. |
suffix | — The trailing zone — a sibling of the link/button, not nested inside it. Overrides the `suffix` prop. |
Leading icon or avatar. Overrides the `icon` prop.
The label region. Overrides the `label` prop; put inline adornments here.
The trailing zone — a sibling of the link/button, not nested inside it. Overrides the `suffix` prop.
SidebarLabel
Show types
import type { Component, ComputedRef, InjectionKey } from 'vue'
import { RouteLocationRaw } from 'vue-router'
import type { AlertAction } from '../Alert'
import type { StatusTheme } from '../shared/statusIcon'
/**
* 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 = {
/** 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
/** Click handler. Bound from `@click`. */
onClick?: (event: MouseEvent) => void
}
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 = {
/** Workspace or app title. */
title: string
/** Secondary line under the title, e.g. a domain or workspace slug. */
subtitle?: string
/** Leading logo: an image URL, or a component. Overridden by the `#prefix` slot. */
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
/** Options rendered in the trigger's dropdown — the same shape `Dropdown` itself takes. */
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* Promotional or onboarding card for the sidebar footer — a trial notice, an
* upgrade prompt, a "what's new" pointer. Stateless: `dismiss` is an event and
* the parent owns hiding the card.
*/
export interface SidebarCardProps {
/** Main heading text of the card. Optional when the `#title` slot is used */
title?: string
/** Supporting text below the title */
description?: string
/** Color theme of the icon and the tinted action button; the white container never changes with theme */
theme?: StatusTheme
/** Icon next to the title: unset shows the theme's auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph */
icon?: boolean | string | Component
/** The full-width tinted action button (`ButtonProps` plus `onClick({ dismiss })`) */
action?: AlertAction
/** Shows the dismiss (×) button, which emits `dismiss` */
dismissible?: boolean
}
export interface SidebarCardEmits {
/** Fired when the user dismisses the card — the × button or the action's `context.dismiss()`. The parent owns hiding. */
dismiss: []
}
/** Scoped payload for the card's `#actions` slot. */
export interface SidebarCardActionsSlotProps {
/** Emits the card's `dismiss` event. */
dismiss: () => void
}
export interface SidebarCardSlots {
/** Overrides the icon area next to the title */
prefix?: () => any
/** Rich title content (overrides the `title` prop) */
title?: () => any
/** Rich description content (overrides the `description` prop) */
description?: () => any
/** Replaces the auto-rendered action button; receives `{ dismiss }` */
actions?: (props: SidebarCardActionsSlotProps) => any
}
/**
* Building block for a collapsible group of `SidebarItem`s. Compose children
* in the default slot — `SidebarSection` owns only the label + collapse
* chrome, not the rows inside it.
*/
export type SidebarSectionProps = {
/** Section label. Renders nothing when omitted (a label-less group). */
label?: string
/** Whether clicking the label toggles the group's visibility. */
collapsible?: boolean
}When true, collapses to a horizontal divider while the sidebar is collapsed (matches the previous `SidebarSection` label behavior).
| Slot | Payload |
|---|---|
default | — The label text. |
The label text.
SidebarHeader
Show types
import type { Component, ComputedRef, InjectionKey } from 'vue'
import { RouteLocationRaw } from 'vue-router'
import type { AlertAction } from '../Alert'
import type { StatusTheme } from '../shared/statusIcon'
/**
* 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 = {
/** 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
/** Click handler. Bound from `@click`. */
onClick?: (event: MouseEvent) => void
}
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 = {
/** Workspace or app title. */
title: string
/** Secondary line under the title, e.g. a domain or workspace slug. */
subtitle?: string
/** Leading logo: an image URL, or a component. Overridden by the `#prefix` slot. */
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
/** Options rendered in the trigger's dropdown — the same shape `Dropdown` itself takes. */
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* Promotional or onboarding card for the sidebar footer — a trial notice, an
* upgrade prompt, a "what's new" pointer. Stateless: `dismiss` is an event and
* the parent owns hiding the card.
*/
export interface SidebarCardProps {
/** Main heading text of the card. Optional when the `#title` slot is used */
title?: string
/** Supporting text below the title */
description?: string
/** Color theme of the icon and the tinted action button; the white container never changes with theme */
theme?: StatusTheme
/** Icon next to the title: unset shows the theme's auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph */
icon?: boolean | string | Component
/** The full-width tinted action button (`ButtonProps` plus `onClick({ dismiss })`) */
action?: AlertAction
/** Shows the dismiss (×) button, which emits `dismiss` */
dismissible?: boolean
}
export interface SidebarCardEmits {
/** Fired when the user dismisses the card — the × button or the action's `context.dismiss()`. The parent owns hiding. */
dismiss: []
}
/** Scoped payload for the card's `#actions` slot. */
export interface SidebarCardActionsSlotProps {
/** Emits the card's `dismiss` event. */
dismiss: () => void
}
export interface SidebarCardSlots {
/** Overrides the icon area next to the title */
prefix?: () => any
/** Rich title content (overrides the `title` prop) */
title?: () => any
/** Rich description content (overrides the `description` prop) */
description?: () => any
/** Replaces the auto-rendered action button; receives `{ dismiss }` */
actions?: (props: SidebarCardActionsSlotProps) => any
}
/**
* Building block for a collapsible group of `SidebarItem`s. Compose children
* in the default slot — `SidebarSection` owns only the label + collapse
* chrome, not the rows inside it.
*/
export type SidebarSectionProps = {
/** Section label. Renders nothing when omitted (a label-less group). */
label?: string
/** Whether clicking the label toggles the group's visibility. */
collapsible?: boolean
}Workspace or app title.
Secondary line under the title, e.g. a domain or workspace slug.
Leading logo: an image URL, or a component. Overridden by the `#prefix` slot.
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.
Options rendered in the trigger's dropdown — the same shape `Dropdown` itself takes.
| Slot | Payload |
|---|---|
prefix | — Fills the default logo/initial box (a `size-7 overflow-hidden` frame — wide content clips). Falls back to the `logo` prop, then the title's first letter. |
Fills the default logo/initial box (a `size-7 overflow-hidden` frame — wide content clips). Falls back to the `logo` prop, then the title's first letter.
SidebarSection
Show types
import type { Component, ComputedRef, InjectionKey } from 'vue'
import { RouteLocationRaw } from 'vue-router'
import type { AlertAction } from '../Alert'
import type { StatusTheme } from '../shared/statusIcon'
/**
* 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 = {
/** 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
/** Click handler. Bound from `@click`. */
onClick?: (event: MouseEvent) => void
}
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 = {
/** Workspace or app title. */
title: string
/** Secondary line under the title, e.g. a domain or workspace slug. */
subtitle?: string
/** Leading logo: an image URL, or a component. Overridden by the `#prefix` slot. */
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
/** Options rendered in the trigger's dropdown — the same shape `Dropdown` itself takes. */
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* Promotional or onboarding card for the sidebar footer — a trial notice, an
* upgrade prompt, a "what's new" pointer. Stateless: `dismiss` is an event and
* the parent owns hiding the card.
*/
export interface SidebarCardProps {
/** Main heading text of the card. Optional when the `#title` slot is used */
title?: string
/** Supporting text below the title */
description?: string
/** Color theme of the icon and the tinted action button; the white container never changes with theme */
theme?: StatusTheme
/** Icon next to the title: unset shows the theme's auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph */
icon?: boolean | string | Component
/** The full-width tinted action button (`ButtonProps` plus `onClick({ dismiss })`) */
action?: AlertAction
/** Shows the dismiss (×) button, which emits `dismiss` */
dismissible?: boolean
}
export interface SidebarCardEmits {
/** Fired when the user dismisses the card — the × button or the action's `context.dismiss()`. The parent owns hiding. */
dismiss: []
}
/** Scoped payload for the card's `#actions` slot. */
export interface SidebarCardActionsSlotProps {
/** Emits the card's `dismiss` event. */
dismiss: () => void
}
export interface SidebarCardSlots {
/** Overrides the icon area next to the title */
prefix?: () => any
/** Rich title content (overrides the `title` prop) */
title?: () => any
/** Rich description content (overrides the `description` prop) */
description?: () => any
/** Replaces the auto-rendered action button; receives `{ dismiss }` */
actions?: (props: SidebarCardActionsSlotProps) => any
}
/**
* Building block for a collapsible group of `SidebarItem`s. Compose children
* in the default slot — `SidebarSection` owns only the label + collapse
* chrome, not the rows inside it.
*/
export type SidebarSectionProps = {
/** Section label. Renders nothing when omitted (a label-less group). */
label?: string
/** Whether clicking the label toggles the group's visibility. */
collapsible?: boolean
}Section label. Renders nothing when omitted (a label-less group).
Whether clicking the label toggles the group's visibility.
v-model. Whether the section is collapsed. Bind it to own the state (start a section collapsed, persist the choice); left unbound the section manages it internally, starting expanded.
| Slot | Payload |
|---|---|
default | — The group's rows — compose `SidebarItem` (or anything else) here. |
The group's rows — compose `SidebarItem` (or anything else) here.
| Event | Payload |
|---|---|
update:collapsed | [value: boolean] Fired when the section is collapsed or expanded. |
Fired when the section is collapsed or expanded.
SidebarCard
Show types
import type { Component, ComputedRef, InjectionKey } from 'vue'
import { RouteLocationRaw } from 'vue-router'
import type { AlertAction } from '../Alert'
import type { StatusTheme } from '../shared/statusIcon'
/**
* 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 = {
/** 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
/** Click handler. Bound from `@click`. */
onClick?: (event: MouseEvent) => void
}
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 = {
/** Workspace or app title. */
title: string
/** Secondary line under the title, e.g. a domain or workspace slug. */
subtitle?: string
/** Leading logo: an image URL, or a component. Overridden by the `#prefix` slot. */
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
/** Options rendered in the trigger's dropdown — the same shape `Dropdown` itself takes. */
menuItems?: {
label: string
icon?: string | Component
onClick?: () => void
}[]
}
/**
* Promotional or onboarding card for the sidebar footer — a trial notice, an
* upgrade prompt, a "what's new" pointer. Stateless: `dismiss` is an event and
* the parent owns hiding the card.
*/
export interface SidebarCardProps {
/** Main heading text of the card. Optional when the `#title` slot is used */
title?: string
/** Supporting text below the title */
description?: string
/** Color theme of the icon and the tinted action button; the white container never changes with theme */
theme?: StatusTheme
/** Icon next to the title: unset shows the theme's auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph */
icon?: boolean | string | Component
/** The full-width tinted action button (`ButtonProps` plus `onClick({ dismiss })`) */
action?: AlertAction
/** Shows the dismiss (×) button, which emits `dismiss` */
dismissible?: boolean
}
export interface SidebarCardEmits {
/** Fired when the user dismisses the card — the × button or the action's `context.dismiss()`. The parent owns hiding. */
dismiss: []
}
/** Scoped payload for the card's `#actions` slot. */
export interface SidebarCardActionsSlotProps {
/** Emits the card's `dismiss` event. */
dismiss: () => void
}
export interface SidebarCardSlots {
/** Overrides the icon area next to the title */
prefix?: () => any
/** Rich title content (overrides the `title` prop) */
title?: () => any
/** Rich description content (overrides the `description` prop) */
description?: () => any
/** Replaces the auto-rendered action button; receives `{ dismiss }` */
actions?: (props: SidebarCardActionsSlotProps) => any
}
/**
* Building block for a collapsible group of `SidebarItem`s. Compose children
* in the default slot — `SidebarSection` owns only the label + collapse
* chrome, not the rows inside it.
*/
export type SidebarSectionProps = {
/** Section label. Renders nothing when omitted (a label-less group). */
label?: string
/** Whether clicking the label toggles the group's visibility. */
collapsible?: boolean
}Main heading text of the card. Optional when the `#title` slot is used
Supporting text below the title
Color theme of the icon and the tinted action button; the white container never changes with theme
Icon next to the title: unset shows the theme's auto icon (gray shows the info glyph in black ink), `false` hides it, a `lucide-*` string or Component renders a custom theme-colored glyph
The full-width tinted action button (`ButtonProps` plus `onClick({ dismiss })`)
Shows the dismiss (×) button, which emits `dismiss`
| Slot | Payload |
|---|---|
prefix | — Overrides the icon area next to the title |
title | — Rich title content (overrides the `title` prop) |
description | — Rich description content (overrides the `description` prop) |
actions | SidebarCardActionsSlotProps Replaces the auto-rendered action button; receives `{ dismiss }` |
Overrides the icon area next to the title
Rich title content (overrides the `title` prop)
Rich description content (overrides the `description` prop)
Replaces the auto-rendered action button; receives `{ dismiss }`
| Event | Payload |
|---|---|
dismiss | [] Fired when the user dismisses the card — the × button or the action's `context.dismiss()`. The parent owns hiding. |
Fired when the user dismisses the card — the × button or the action's `context.dismiss()`. The parent owns hiding.