Frappe UIFrappe UI

Rail

The narrow icon column of an app shell. Rail is a bare frame — a fixed 50px column with one shared tooltip context and a single slot — and RailItem 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.

Content
vue
<script setup lang="ts">
import { ref } from 'vue'
import { Rail, RailItem, 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-md border bg-surface-base">
    <Rail>
      <RailItem 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">
        <RailItem
          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>
        </RailItem>
      </div>

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

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

RailItem

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

  • variant="tile" (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 to to render a router link; omit it to get a button that emits click. 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

Rail

default
{}

RailItem

Show types
typescript
import type { Component } from 'vue'
import type { RouteLocationRaw } from 'vue-router'

export interface RailItemProps {
  /** Tooltip text and the base of the item's accessible label. */
  label: 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.
   */
  to?: RouteLocationRaw

  /** 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.
   * - `tile` (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?: 'tile' | 'ghost'
}
label*
string

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

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

to
string | kt | Tt

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

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
= "tile"
"ghost" | "tile"

Visual treatment. - `tile` (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
{}
click
[event: MouseEvent]