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

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

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

  • SidebarItem directly instead.

Pipeline

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

Your trial ends soon!

Upgrade to keep enjoying features.

New feature available

Discover the new board view for your deals.

Storage is almost full

Free up space or upgrade your plan.

Payment failed

Update your card to avoid interruption.

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

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

v-model. Whether the sidebar is collapsed. Left unset, it collapses automatically below the `sm` breakpoint.

default

The sidebar body — header, scroll region, footer, all composed by the app.

update:collapsed
[value: boolean | null]

Fired when the sidebar is collapsed or expanded.

SidebarItem

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

onClick
((event: MouseEvent) => void)

Click handler. Bound from `@click`.

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.

SidebarLabel

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

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

default

The label text.

SidebarHeader

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

Workspace or app title.

subtitle
string

Secondary line under the title, e.g. a domain or workspace slug.

logo
string | Component

Leading logo: an image URL, or a component. Overridden by the `#prefix` slot.

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

Options rendered in the trigger's dropdown — the same shape `Dropdown` itself takes.

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.

SidebarSection

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

Section label. Renders nothing when omitted (a label-less group).

collapsible
boolean

Whether clicking the label toggles the group's visibility.

collapsed
= false
boolean

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.

default

The group's rows — compose `SidebarItem` (or anything else) here.

update:collapsed
[value: boolean]

Fired when the section is collapsed or expanded.

SidebarCard

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

Main heading text of the card. Optional when the `#title` slot is used

description
string

Supporting text below the title

theme
= "gray"
StatusTheme

Color theme of the icon and the tinted action button; the white container never changes with theme

icon
boolean | string | Component

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

action
AlertAction

The full-width tinted action button (`ButtonProps` plus `onClick({ dismiss })`)

dismissible
= false
boolean

Shows the dismiss (×) button, which emits `dismiss`

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

dismiss
[]

Fired when the user dismisses the card — the × button or the action's `context.dismiss()`. The parent owns hiding.