# AppRail

> The leftmost desktop column, on the frame, under the web top bar — the product's destinations at the top, help and settings at the bottom.

Source: https://raft-design.botiverse.build/components/app-rail. Generated from the current release; where it differs from what you remember, this page is right.

AppRail is the desktop's leftmost column of destinations, on the yellow frame, under the web top bar. The product's destinations are at the top, and help and settings sit at the bottom; the server and the bell are in the top bar ([Page structure](https://raft-design.botiverse.build/patterns/page-structure.md#the-web-top-bar)). Selecting a destination fills the [Sidebar](https://raft-design.botiverse.build/components/sidebar.md) next to it.

```tsx
import { useState } from "react";
import type { ComponentProps, ReactNode } from "react";
import {
  AppShellRailSlot,
  AppRailFooter,
  AppRailItem,
  AppRailItemAttention,
  AppRailItemIcon,
  AppRailItemLabel,
  AppRailNav,
  AppRailRoot,
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@botiverse/raft-ui";
import {
  Activity,
  CheckSquare,
  MessageSquare,
  Monitor,
  Settings,
  Users,
  CircleHelp,
} from "lucide-react";

// Tooltip-wrapped desktop rail item. Callers own the rendered navigation or action target.
export function AppRailNavItem({
  nativeButton,
  selected,
  label,
  icon,
  attention,
  render,
}: {
  nativeButton?: boolean;
  selected?: boolean;
  label: string;
  icon: ReactNode;
  attention?: ReactNode;
  render: ComponentProps<typeof AppRailItem>["render"];
}) {
  return (
    <Tooltip>
      <TooltipTrigger
        render={<AppRailItem nativeButton={nativeButton} selected={selected} render={render} />}
      >
        <AppRailItemIcon>
          {icon}
          {attention}
        </AppRailItemIcon>
        <AppRailItemLabel>{label}</AppRailItemLabel>
      </TooltipTrigger>
      <TooltipContent side="right">{label}</TooltipContent>
    </Tooltip>
  );
}

export function AppRailWithTopBarComposition() {
  const [view, setView] = useState("chat");

  return (
    <AppShellRailSlot>
      <AppRailRoot>
        <TooltipProvider delay={650}>
          <AppRailNav>
            <AppRailNavItem
              selected={view === "chat"}
              render={<button type="button" onClick={() => setView("chat")} />}
              label="Chat"
              icon={<MessageSquare />}
            />
            <AppRailNavItem
              selected={view === "activity"}
              render={<button type="button" onClick={() => setView("activity")} />}
              label="Activity"
              icon={<Activity />}
              attention={<AppRailItemAttention title="Unread activity" />}
            />
            <AppRailNavItem
              selected={view === "tasks"}
              render={<button type="button" onClick={() => setView("tasks")} />}
              label="Tasks"
              icon={<CheckSquare />}
            />
            <AppRailNavItem
              selected={view === "members"}
              render={<button type="button" onClick={() => setView("members")} />}
              label="Members"
              icon={<Users />}
            />
            <AppRailNavItem
              selected={view === "computers"}
              render={<button type="button" onClick={() => setView("computers")} />}
              label="Computers"
              icon={<Monitor />}
              attention={<AppRailItemAttention title="A computer needs attention" />}
            />
          </AppRailNav>
          <AppRailFooter>
            <AppRailNavItem render={<button type="button" />} label="Help" icon={<CircleHelp />} />
            <AppRailNavItem
              selected={view === "settings"}
              render={<button type="button" onClick={() => setView("settings")} />}
              label="Settings"
              icon={<Settings />}
            />
          </AppRailFooter>
        </TooltipProvider>
      </AppRailRoot>
    </AppShellRailSlot>
  );
}
```

## When to use

| Situation                              | Use                                      |
| -------------------------------------- | ---------------------------------------- |
| A product's destinations on desktop    | AppRail                                  |
| The same destinations on a phone       | [MobileNav](https://raft-design.botiverse.build/components/mobile-nav.md) |
| What the selected destination contains | [Sidebar](https://raft-design.botiverse.build/components/sidebar.md)         |

Which destinations a product has, and in which order, is on its own page ([Raft Chat](https://raft-design.botiverse.build/products/raft-chat.md)).

## Anatomy

- `AppRailRoot`: the column, on `layer-frame`, with a `line-seam` along its right edge.
- `AppRailHeader`: the 56px square at the top, holding the workspace tile. Its seam lines up with the sidebar's and the panel's ([Page structure](https://raft-design.botiverse.build/patterns/page-structure.md#headers)). It is left out when the web top bar is shown.
- `AppRailNav`: the destinations.
- `AppRailItem`: one destination: an `AppRailItemIcon` with a 20px icon, and an `AppRailItemLabel` with its name.
- `AppRailItemAttention`: the pink pixel on an item, with text for assistive technology. It is built on `AppRailItemIndicator`.
- `AppRailItemBadge`: a count in the same corner. Not recommended, and to be removed: a rail icon shows only the dot, never a number ([Badge](https://raft-design.botiverse.build/components/badge.md#rules)).
- `AppRailFooter`: notifications, help and settings. Notifications sit here only when there is no web top bar; with it, the bell is in the top bar ([Navigation](https://raft-design.botiverse.build/patterns/navigation.md#structure)).

Without the web top bar (an older desktop build, the app embedded in another host, a server's public page), the rail has its header with the workspace tile, search is a destination and the bell sits in the footer:

```tsx
import { useCallback, useState } from "react";
import type { ComponentProps, ReactNode } from "react";
import {
  AppShellRailSlot,
  AppRailFooter,
  AppRailHeader,
  AppRailItem,
  AppRailItemAttention,
  AppRailItemIcon,
  AppRailItemIndicator,
  AppRailItemLabel,
  AppRailNav,
  AppRailRoot,
  NotificationCenter,
  NotificationCenterTrigger,
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@botiverse/raft-ui";
import {
  Activity,
  CheckSquare,
  MessageSquare,
  Monitor,
  Search,
  Settings,
  Users,
} from "lucide-react";

import { AppRailServerButton } from "#/modules/app-rail/app-rail-server-button.tsx";

// Tooltip-wrapped desktop rail item. Callers own the rendered navigation or action target.
function AppRailNavItem({
  nativeButton,
  selected,
  label,
  icon,
  attention,
  render,
}: {
  nativeButton?: boolean;
  selected?: boolean;
  label: string;
  icon: ReactNode;
  attention?: ReactNode;
  render: ComponentProps<typeof AppRailItem>["render"];
}) {
  return (
    <Tooltip>
      <TooltipTrigger
        render={<AppRailItem nativeButton={nativeButton} selected={selected} render={render} />}
      >
        <AppRailItemIcon>
          {icon}
          {attention}
        </AppRailItemIcon>
        <AppRailItemLabel>{label}</AppRailItemLabel>
      </TooltipTrigger>
      <TooltipContent side="right">{label}</TooltipContent>
    </Tooltip>
  );
}

export function AppRailWithoutTopBarComposition() {
  const [view, setView] = useState("chat");
  const [otherServersUnread, setOtherServersUnread] = useState(true);

  const handleToggleOtherServersUnread = useCallback(() => {
    setOtherServersUnread((current) => !current);
  }, []);

  return (
    <AppShellRailSlot>
      <AppRailRoot>
        <AppRailHeader>
          <AppRailServerButton
            title="Raft 中文社区"
            aria-label={
              otherServersUnread
                ? "Raft 中文社区. Mark other servers read"
                : "Raft 中文社区. Mark other servers unread"
            }
            onClick={handleToggleOtherServersUnread}
          >
            <img alt="" src="/avatars/workspace.svg" />
            {otherServersUnread ? <AppRailItemIndicator size="sm" aria-hidden /> : null}
          </AppRailServerButton>
        </AppRailHeader>
        <TooltipProvider delay={650}>
          <AppRailNav>
            <AppRailNavItem
              selected={view === "search"}
              render={<button type="button" onClick={() => setView("search")} />}
              label="Search"
              icon={<Search />}
            />
            <AppRailNavItem
              selected={view === "chat"}
              render={<button type="button" onClick={() => setView("chat")} />}
              label="Chat"
              icon={<MessageSquare />}
            />
            <AppRailNavItem
              selected={view === "activity"}
              render={<button type="button" onClick={() => setView("activity")} />}
              label="Activity"
              icon={<Activity />}
              attention={<AppRailItemAttention title="Unread activity" />}
            />
            <AppRailNavItem
              selected={view === "tasks"}
              render={<button type="button" onClick={() => setView("tasks")} />}
              label="Tasks"
              icon={<CheckSquare />}
            />
            <AppRailNavItem
              selected={view === "members"}
              render={<button type="button" onClick={() => setView("members")} />}
              label="Members"
              icon={<Users />}
            />
            <AppRailNavItem
              selected={view === "computers"}
              render={<button type="button" onClick={() => setView("computers")} />}
              label="Computers"
              icon={<Monitor />}
              attention={<AppRailItemAttention title="A computer needs attention" />}
            />
          </AppRailNav>
          <AppRailFooter>
            <NotificationCenter modal={false}>
              <NotificationCenterTrigger type="button" aria-label="Notification center" />
            </NotificationCenter>
            <AppRailNavItem
              selected={view === "settings"}
              render={<button type="button" onClick={() => setView("settings")} />}
              label="Settings"
              icon={<Settings />}
            />
          </AppRailFooter>
        </TooltipProvider>
      </AppRailRoot>
    </AppShellRailSlot>
  );
}
```

## Variants

| Variant              | Use for                                                                                                                                                                                                                                                |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Icons only (default) | Every product today. 56px wide plus the seam; each item is 40px square, and its name appears in a tooltip to the right                                                                                                                                 |
| Icons with labels    | Names under the icons, in `text-label` (bold when selected). 68px wide plus the seam; each item is 60×52px, and the selected sticker wraps the icon and the label. The footer tools keep icons only, because "Notifications" doesn't fit under an icon |

The product ships the icons-only rail, and raft-ui implements only that one ([Open questions](https://raft-design.botiverse.build/decisions/open-questions.md#settled-by-the-product)). A labeled rail can't keep a square top-left corner (it is 68×56px), and the product pins that square with a UI test (`leftrail-square-slot`).

## States

- Selected: a sticker on the frame, white in light mode and yellow in dark mode ([States](https://raft-design.botiverse.build/foundations/states.md)).
- Attention: one pink pixel at the top right of the icon, never a count. The selected item never shows it, with no exception.
- Hover and press lay `fill-frame-medium` over the item, the medium tier on the frame ([Hover and press](https://raft-design.botiverse.build/foundations/states.md#hover-and-press)). The selected item doesn't change on hover.

## Behavior

- Space every item, in the nav and in the footer, the same way: 40px items, 6px apart, 8px from both ends of the rail. Don't wrap the footer tools in fixed-height cells.
- Under the header, start the first item 8px below the header's seam, the same 8px as under the web top bar and at the bottom of the rail, so the rail has one inset.
- With the web top bar, the rail has no header and the server moves to the top bar, as the workspace button ([Page structure](https://raft-design.botiverse.build/patterns/page-structure.md#the-web-top-bar)).
- In a window shorter than 600px, the rail shrinks with the headers ([Responsive layout](https://raft-design.botiverse.build/patterns/responsive.md)), and its items keep 6px from both ends.

## Content

- Name every item, even in the icons-only rail. Names are Title Case ([Typography](https://raft-design.botiverse.build/foundations/typography.md#casing)).

## Accessibility

- `AppRailNav` renders a `nav`. Name it with `aria-label="Primary"`.
- `AppRailItem` renders the Base UI Button; `selected` sets `aria-pressed`. For an item that navigates, render a link (`render={<a href="…" />}` with `nativeButton={false}`) and set `aria-current="page"` on the selected one: raft-ui sets neither on a link.
- `AppRailItemLabel` is visually hidden and names the item. Repeat the name in a [Tooltip](https://raft-design.botiverse.build/components/tooltip.md) on the right for pointer users.
- Say the attention dot in the item's name with `AppRailItemAttention`'s `title`: "Activity, new". The pixel itself is `aria-hidden`.
- Name the workspace tile after the server and what it does: "Raft HQ, switch server".

## Rules

- Make the workspace tile ink with the server's initial in yellow. It opens the server list next to the tile ([Built from existing parts](https://raft-design.botiverse.build/components.md#switching-servers)).

## API

### AppRailFooter

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### AppRailHeader

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### AppRailItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `nativeButton` | `boolean` | `true` |  |
| `selected` | `boolean` |  |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### AppRailItemAttention

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` |  | Read to assistive tech; the pixel itself is decorative. |

### AppRailItemBadge

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### AppRailItemIcon

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### AppRailItemIndicator

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `presence` | `"online" \| "busy" \| "error" \| "offline" \| "external"` |  | A status light (online, busy, error, offline, external): a fixed meaning that needs its word nearby, or an aria-label. It takes the place of `variant`'s colour. |
| `size` | `"xs" \| "sm" \| "md" \| "lg"` | `"sm"` |  |
| `variant` | `"primary" \| "accent" \| "warning" \| "danger" \| "default" \| "information" \| "muted" \| "success"` |  |  |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### AppRailItemLabel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### AppRailNav

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

### AppRailRoot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| ((props, state) => ReactElement)` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component. |

Also takes the attributes of its element.

## Changes

- 0.6.0 (Breaking Changes): **Props that no longer change anything are removed:** `Badge` `uppercase`; `Checkbox` `size`, `variant`, `color`; `Status` `pulse`, `attention` (status never animates); `Switch` / `SwitchThumb` `size`; `SidebarItem` `variant`; `SidebarItemCount` / `SidebarItemIndicator` `variant` and `appearance` (the count is always the pink sticker, the indicator the yellow @); `NotificationCenterActionButton` `variant`; `DrawerContent` `width` (one-go forms use the wide `Dialog`); `AppRailItemIndicator` `attention`; `AppRailItemAttention` `children`; `Steps` `size`, `track`, `renderIndicator`, `renderTraveler`; `FilePreviewBadge` `type`, `color`. Remove them at call sites.
- 0.4.0 (Changed): **AppShell no longer draws column dividers.** `AppShellRailSlot` and `AppShellSidebarSlot` dropped their border classes in both themes. The rail|sidebar divider now lives on `AppRailRoot` (brutal); seam borders the shell used to own (e.g. sidebar|main with an inset main panel) must be added by the caller. ([e219fd8](https://github.com/botiverse/rui/commit/e219fd8))
