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.
<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 aniconfor 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
| Slot | Payload |
|---|---|
default | {} |
RailItem
Show types
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'
}Tooltip text and the base of the item's accessible label.
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).
Navigation target. When set, the item renders as a router link; otherwise it renders as a button. A `click` event fires in both cases.
Marks the item as the current destination (indicator bar / active fill).
Unread count. Drives the badge and folds into the accessible label.
How the badge renders: a count pill or a small 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.
| Slot | Payload |
|---|---|
default | {} |
| Event | Payload |
|---|---|
click | [event: MouseEvent] |