SidebarRail
The narrow icon column of an app shell. For a wide column with labels, use Sidebar.
<script setup lang="ts">
import { ref } from 'vue'
import { SidebarRail, SidebarRailItem, Avatar } from 'frappe-ui'
const active = ref('design')
function logo(seed: string) {
return `https://api.dicebear.com/9.x/shapes/svg?seed=${seed}`
}
const communities = [
{ id: 'design', label: 'Design', badge: 0, image: logo('design') },
{
id: 'engineering',
label: 'Engineering',
badge: 3,
image: logo('engineering'),
},
{ id: 'marketing', label: 'Marketing', badge: 0, image: logo('marketing') },
{ id: 'sales', label: 'Sales', badge: 0, image: null },
]
</script>
<template>
<div
class="flex h-[420px] w-fit 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"
>
<!-- The item paints no fill under an image. Initials sit on the
item's own fill. -->
<img
v-if="c.image"
:src="c.image"
alt=""
class="size-7 rounded-[7px]"
/>
<span v-else class="text-2xs-medium uppercase text-ink-gray-5">
{{ c.label.slice(0, 2) }}
</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"
image="https://api.dicebear.com/9.x/notionists/svg?seed=Jane&backgroundColor=c0aede"
size="md"
/>
</SidebarRailItem>
</SidebarRail>
</div>
</template>Anatomy
SidebarRail is a fixed 50px column with one slot. SidebarRailItem is one cell in it, with a tooltip. The rail has no layout slots and does not scroll, so lay out the children with flex classes. Give the middle group flex-1 to push the items above and below it to the ends.
<SidebarRail>
<SidebarRailItem label="Home" variant="ghost" icon="lucide-house" route="/" />
<div class="flex w-full flex-1 flex-col items-center gap-3 overflow-y-auto pt-3">
<SidebarRailItem label="Design" route="/c/design" :badge="3">
<span>DE</span>
</SidebarRailItem>
</div>
<SidebarRailItem label="Search" variant="ghost" icon="lucide-search" />
<SidebarRailItem label="You" variant="ghost">
<Avatar label="Jane Doe" size="md" />
</SidebarRailItem>
</SidebarRail>Behavior
Rail and sidebar
SidebarRail shares the Sidebar name because the two sit side by side in the same app frame, not because one contains the other. Use 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. Sidebar collapses by itself.
Layout and scrolling
Put fixed items, such as a logo or a user menu, directly in the rail. If the middle list can overflow, wrap it in your own overflow-y-auto container.
Tooltips
Each item shows its label in a tooltip to the right. description adds a second line, such as "12 members". The rail shares one tooltip context across its items, so moving between items shows each tooltip with no delay.
Variants
variant="subtle"(default) is a filled cell with a bar on its left edge when active. Use the default slot for an image, an avatar or initials. When the slot holds an<img>, the cell paints no fill, so the image's rounded corners show no gray rim. The bar still marks the active item.variant="ghost"is transparent until hovered, and raised when active. Pass aniconfor a shortcut such as Search or Notifications.
Links
route renders a router link and href a plain same-tab link. route wins when both are set. A string route falls back to a plain link when no router is installed. An item with neither is a button. All three emit click.
When active is unset, a routed item is active when its route matches the current route.
Unread badge
badge takes an unread count. badgeStyle="count" (default) shows it as a pill, and counts above 99 show as "99+". badgeStyle="dot" shows a dot, and the tooltip spells out the number unless description is set. The pill is rendered outside the rail, so an overflow-hidden container does not clip it.
Attributes
Attributes on SidebarRailItem go on the clickable cell (the <button> or the link), not on the tooltip wrapper. A class adds to the cell's own classes.
Accessibility
Each item uses its label as its accessible name. When badge is above zero, the count is added to the name, for example "Notifications, 3 unread". The active item gets aria-current="page".
API Reference
SidebarRail
| Slot | Payload |
|---|---|
default | — `SidebarRailItem`s, and any fixed anchors (a logo, a user menu). Layout is plain flex — no built-in scrolling. |
`SidebarRailItem`s, and any fixed anchors (a logo, a user menu). Layout is plain flex — no built-in scrolling.
SidebarRailItem
Show types
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. The cell paints
* no fill when its default slot holds an `<img>`.
* - `ghost`: transparent until hovered, a raised highlight when active — for
* icon shortcuts like Search or Notifications.
*/
variant?: 'subtle' | 'ghost'
}Tooltip text and the base of the item's accessible label.
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 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.
External URL. Used when `route` is absent; renders a native same-tab anchor.
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. - `subtle` (default): a filled cell with a left indicator bar when active — for image/avatar items like communities or workspaces. The cell paints no fill when its default slot holds an `<img>`. - `ghost`: transparent until hovered, a raised highlight when active — for icon shortcuts like Search or Notifications.
| Slot | Payload |
|---|---|
default | — Custom content in place of the default icon — an image, avatar, or initials. |
Custom content in place of the default icon — an image, avatar, or initials.
| Event | Payload |
|---|---|
click | [event: MouseEvent] |