Rail
betaThe app's left rail, 199px wide: your app's name, Search, places grouped with their counts, one pinned at the bottom, and the workspace you're in.
Preview
Home
Press Search, or ⌘K.
No search, no workspace: just the places.
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/rail
Use it for
- The left edge of an app with a handful of places: Home, Messages, each team, Projects.
- Group places with a title, like "Teams". Give a place a badge for its count, in tone "warning" when it needs someone, or "brand" for news.
- Give each place an href to make it a link, or leave it off and handle onSelect. Pass active for the place you're in.
- Pass onSearch to show Search at the top. ⌘K (Ctrl+K) opens it from anywhere but a text field. If the page has a Command Menu, give that shortcut={false}, so one ⌘K opens one search.
- Put Settings in footer, so it stays at the bottom however many places there are. Put the workspace you're in, with its colors, in club.
- The rail fills its parent's height. Give the parent a height, or pass a className like h-dvh.
Avoid it for
- Phones. A 199px rail leaves too little room. Use a bottom bar or a sheet there.
- Places inside one page. Use tabs.
- Switching workspaces. Put a switcher in header.
Props
- groups
Required. RailItem is { id, label, icon?, href?, badge?, tone? }. icon is a name from Icon's set or any icon. tone is "neutral", "warning", or "brand".
{ title?: string; items: RailItem[] }[]
- active
The id of the place you're in. Controlled, with onSelect.
string
- defaultActive
The place you start in, when active is not set.
string
- onSelect
Runs with the item's id when you press it.
(id: string) => void
- onSearch
Opens search. Shows Search at the top and listens for ⌘K (Ctrl+K).
() => void
- label
The navigation's name for screen readers.
string · default "Main"
- header
The top of the rail, like your app's name or a switcher.
ReactNode
- footer
One item pinned at the bottom, like Settings.
RailItem
- club
The workspace you're in, at the foot, with its colors as a TeamFlag.
{ label: string; colors?: [string, string]; pattern?: "hoops" | "halves" | "thirds" | "split" }
- alerts
Its open alerts, as a count beside its name.
number
- className
Extra classes on the rail. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| groups | { title?: string; items: RailItem[] }[] | - | Required. RailItem is { id, label, icon?, href?, badge?, tone? }. icon is a name from Icon's set or any icon. tone is "neutral", "warning", or "brand". |
| active | string | - | The id of the place you're in. Controlled, with onSelect. |
| defaultActive | string | - | The place you start in, when active is not set. |
| onSelect | (id: string) => void | - | Runs with the item's id when you press it. |
| onSearch | () => void | - | Opens search. Shows Search at the top and listens for ⌘K (Ctrl+K). |
| label | string | "Main" | The navigation's name for screen readers. |
| header | ReactNode | - | The top of the rail, like your app's name or a switcher. |
| footer | RailItem | - | One item pinned at the bottom, like Settings. |
| club | { label: string; colors?: [string, string]; pattern?: "hoops" | "halves" | "thirds" | "split" } | - | The workspace you're in, at the foot, with its colors as a TeamFlag. |
| alerts | number | - | Its open alerts, as a count beside its name. |
| className | string | - | Extra classes on the rail. Later classes win. |
Accessibility
- The rail is a navigation landmark named "Main", or label. Each group is a list named by its title.
- Places are links, or buttons without an href. Each is a Tab stop, top to bottom. The place you're in says aria-current="page".
- The place you're in is a raised card and bold. Forced-colors mode draws an outline around it.
- Search says its shortcut in aria-keyshortcuts. ⌘K never fires while you type in a field.
- The wash follows the pointer and the keyboard. It never shows for touch, and it moves at once with reduced motion.
- Headings, Search, and icons hold 4.5:1 on the rail's gray and under the wash, in both themes.
Examples
Grouped places, Search, and a pinned footer
<div className="flex h-[580px]">
<Rail
header={<span className="font-semibold">Your company</span>}
groups={[
{ items: [{ id: "home", label: "Home", icon: "house", href: "/" }, { id: "messages", label: "Messages", icon: "message-circle", href: "/messages", badge: 3, tone: "brand" }] },
{ title: "Teams", items: [{ id: "design", label: "Design", icon: "users", href: "/teams/design" }] },
]}
active="home"
onSearch={() => console.log("search")}
footer={{ id: "settings", label: "Settings", icon: "settings", href: "/settings" }}
club={{ label: "Your team", colors: ["#1e3a8a", "#ffffff"], pattern: "hoops" }}
alerts={2}
/>
</div>