Skip to content
txbrendan/ui

Rail

beta

The 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. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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

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>