Frappe UIFrappe UI

SidebarRail

The narrow icon column of an app shell. SidebarRail is a bare frame — a fixed 50px column with one shared tooltip context and a single slot — and SidebarRailItem is one tooltip'd cell inside it. Lay the children out with plain flex utilities: give the middle section flex-1 to push the anchors above and below it to the edges.

It shares the Sidebar name because the two sit side by side in the same app frame, not because one contains the other. Compose them independently: a rail with a Sidebar beside it, a rail on its own, or a sidebar on its own. The rail is not a collapsed Sidebar — collapsing is Sidebar's own behaviour.

Content
vue
<script setup lang="ts">
import { ref } from 'vue'
import { SidebarRail, SidebarRailItem, Avatar } from 'frappe-ui'

const active = ref('design')
const communities = [
  { id: 'design', label: 'Design', initials: 'DE', badge: 0 },
  { id: 'engineering', label: 'Engineering', initials: 'EN', badge: 3 },
  { id: 'marketing', label: 'Marketing', initials: 'MK', badge: 0 },
]
</script>

<template>
  <div class="flex h-[420px] overflow-hidden rounded-5 border bg-surface-base">
    <SidebarRail>
      <SidebarRailItem label="Home" variant="ghost" icon="lucide-house" @click="active = ''" />

      <!-- The consumer lays the middle out; flex-1 pushes the rest to the bottom. -->
      <div class="flex w-full flex-1 flex-col items-center gap-3 pt-3">
        <SidebarRailItem
          v-for="c in communities"
          :key="c.id"
          :label="c.label"
          :active="active === c.id"
          :badge="c.badge"
          badge-style="dot"
          @click="active = c.id"
        >
          <span class="text-2xs-medium uppercase text-ink-gray-5">{{ c.initials }}</span>
        </SidebarRailItem>
      </div>

      <SidebarRailItem label="Search" variant="ghost" icon="lucide-search" />
      <SidebarRailItem label="Notifications" variant="ghost" icon="lucide-bell" :badge="5" />
      <SidebarRailItem label="You" variant="ghost" class="mt-1">
        <Avatar label="Jane Doe" size="md" />
      </SidebarRailItem>
    </SidebarRail>

    <div class="flex flex-1 items-center justify-center text-ink-gray-5">
      Content
    </div>
  </div>
</template>

There are no layout slots and no built-in scrolling — position is CSS. Put fixed anchors (a logo, a user menu) as direct children, and if the middle list can overflow, wrap it in your own overflow-y-auto container.

SidebarRailItem

SidebarRailItem carries the tooltip (its label), the active indicator, and an optional unread badge. Two visual treatments:

  • variant="subtle" (default) — a filled cell with a left indicator bar when active. Use the default slot for an image, avatar, or initials.
  • variant="ghost" — transparent until hovered, raised when active. Pass an icon for a shortcut like Search or Notifications.

Set route to render a router link or href for a native same-tab anchor; route takes precedence when both are set. A string route falls back to a plain anchor when no router is installed. Omit both to get a button that emits click. When active is omitted, a routed item derives it from the current route. The badge count shows as a pill (badgeStyle="count") or a dot (badgeStyle="dot"); either way it folds into the item's accessible label, and a dot surfaces the real number in the tooltip. The badge pill teleports to <body> so an overflow-hidden scroll container can't clip it.

API Reference

SidebarRail

default

`SidebarRailItem`s, and any fixed anchors (a logo, a user menu). Layout is plain flex — no built-in scrolling.

SidebarRailItem

Show types
typescript
import type { Component } from 'vue'
import type { RouteDestination } from '../shared/route'

export interface SidebarRailItemProps {
  /** Tooltip text and the base of the item's accessible label. */
  label: string

  /**
   * Second line in the tooltip, under the label — e.g. "12 members". Overrides the
   * unread count a `dot` badge would otherwise spell out there.
   */
  description?: string

  /**
   * Icon CSS class, e.g. `lucide-search`, or a component. Rendered centered in the cell.
   * Ignored when the default slot is used (for an image, avatar, or initials).
   */
  icon?: string | Component

  /**
   * Navigation target. When set, the item renders as a router link; otherwise
   * it renders as a button. A `click` event fires in both cases.
   */
  route?: RouteDestination

  /** External URL. Used when `route` is absent; renders a native same-tab anchor. */
  href?: string

  /** Marks the item as the current destination (indicator bar / active fill). */
  active?: boolean

  /** Unread count. Drives the badge and folds into the accessible label. */
  badge?: number

  /** How the badge renders: a count pill or a small dot. */
  badgeStyle?: 'count' | 'dot'

  /**
   * Visual treatment.
   * - `subtle` (default): a filled cell with a left indicator bar when active —
   *   for image/avatar items like communities or workspaces.
   * - `ghost`: transparent until hovered, a raised highlight when active — for
   *   icon shortcuts like Search or Notifications.
   */
  variant?: 'subtle' | 'ghost'
}
label*
string

Tooltip text and the base of the item's accessible label.

description
string

Second line in the tooltip, under the label — e.g. "12 members". Overrides the unread count a `dot` badge would otherwise spell out there.

icon
string | Component

Icon CSS class, e.g. `lucide-search`, or a component. Rendered centered in the cell. Ignored when the default slot is used (for an image, avatar, or initials).

route
RouteDestination

Navigation target. When set, the item renders as a router link; otherwise it renders as a button. A `click` event fires in both cases.

href
string

External URL. Used when `route` is absent; renders a native same-tab anchor.

active
boolean

Marks the item as the current destination (indicator bar / active fill).

badge
= 0
number

Unread count. Drives the badge and folds into the accessible label.

badgeStyle
= "count"
"count" | "dot"

How the badge renders: a count pill or a small dot.

variant
= "subtle"
"subtle" | "ghost"

Visual treatment. - `subtle` (default): a filled cell with a left indicator bar when active — for image/avatar items like communities or workspaces. - `ghost`: transparent until hovered, a raised highlight when active — for icon shortcuts like Search or Notifications.

default

Custom content in place of the default icon — an image, avatar, or initials.

click
[event: MouseEvent]