Frappe UIFrappe UI

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.

Spaces

vue
<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.

Mail

vue
<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 the icon prop: a lucide class, text, or a component).
  • default slot — the label region (falls back to the label prop). 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.

vue
<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

Show types
typescript
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
}
header

Deprecated — Config-object header. Prefer composing your own header in the default slot. Kept for one release for backward compatibility.

sections

Deprecated — Config-object sections. Prefer composing `SidebarLabel` + `SidebarItem` in the default slot. Kept for one release.

disableCollapse
boolean

Disables collapsing entirely (fixed width, no built-in toggle).

width
= "15rem"
string

Expanded width as a CSS length. Applied inline so apps can override it.

collapsedWidth
= "3rem"
string

Collapsed width as a CSS length.

collapsed
= null
boolean | null
default
{}
header-logo
{}
footer-items
{}
update:collapsed
[value: boolean | null]

Fired when the collapsed changes.

SidebarItem

Show types
typescript
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
}
label
string

Row label. Used as the accessible name and the default slot fallback.

accessKey
string

`accesskey` attribute for a keyboard shortcut.

icon
string | Component

Leading icon: a CSS class (e.g. `lucide-box`), plain text, or a component. Ignored when the `#prefix` slot is used.

suffix
string

Trailing text. Ignored when the `#suffix` slot is used.

to
string | kt | Tt

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`.

active
boolean

Marks the row active (`data-state="active"`). When omitted, active state is inferred by matching `to` against the current route.

isActive

Deprecated — Use `active`. Alias kept for the config-object path.

onClick
((event: MouseEvent) => void)

Click handler. Bound from `@click` in composition and from `item.onClick` in the config-object path — both resolve to this prop.

condition

Deprecated — Config-object visibility flag; filtered by the legacy adapter.

prefix
{}
default
{}
suffix
{}

SidebarLabel

Show types
typescript
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
}
divider
boolean

When true, collapses to a horizontal divider while the sidebar is collapsed (matches the previous `SidebarSection` label behavior).

default
{}

SidebarHeader

Show types
typescript
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
}
title*
string
subtitle
string
logo
string | Component
showLogo
= true
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.

menuItems
{ label: string; icon?: string | Component; onClick?: (() => void) | undefined; }[] | undefined
logo
{}

SidebarSection

Show types
typescript
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
}
label
string
items*
SidebarItemProps[]
collapsible
boolean
sidebar-item
{ item: SidebarItemProps; isCollapsed: boolean; }