Documentation menu

rail

Left-edge section minimap: one tick per heading, active-section highlight, labels on hover. Hidden below 1280px.

navigationscrollspytoc

Preview

First section

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Second section

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Third section

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Widen the window past 1280px and scroll — the tick on the left edge follows the section you are reading, and hovering the rail reveals the labels.

Installation

Ask Claude (MCP)

With the MCP server configured, just ask:

Claude
add the moco rail component

shadcn CLI

Terminal
npx shadcn@latest add https://mocoui.site/r/rail.json

Manual

Copy each file from the Source section into its target path:

registry/rail/rail.tsx  →  components/moco/rail.tsx
registry/rail/rail.css  →  components/moco/rail.css

Needs moco items: tokens, hooks

Usage

usage.tsx
"use client";

import { Rail, type Heading } from "./rail";

const HEADINGS: Heading[] = [
  { id: "one", text: "First section" },
  { id: "two", text: "Second section" },
  { id: "three", text: "Third section" },
];

export default function RailDemo() {
  return (
    <div>
      <Rail headings={HEADINGS} />
      <main style={{ maxWidth: "42rem", margin: "0 auto", padding: "4rem 1rem" }}>
        {HEADINGS.map((h) => (
          <section key={h.id}>
            <h2 id={h.id}>{h.text}</h2>
            {Array.from({ length: 12 }, (_, i) => (
              <p key={i}>
                Widen the window past 1280px and scroll — the tick on the left edge follows the
                section you are reading, and hovering the rail reveals the labels.
              </p>
            ))}
          </section>
        ))}
      </main>
    </div>
  );
}

Source

components/moco/rail.tsx
"use client";

import * as React from "react";
import "./rail.css";
import { useScrollspy, smoothScrollTo } from "@/components/moco/hooks";

export interface Heading {
  id: string;
  text: string;
  level?: number;
}

export interface RailProps {
  headings: Heading[];
}

/** Left-edge minimap: one tick per H2. Hidden below 1280px. */
export function Rail({ headings }: RailProps) {
  const ids = React.useMemo(() => headings.map((h) => h.id), [headings]);
  const active = useScrollspy(ids);
  if (!headings.length) return null;

  return (
    <nav className="moco-rail" aria-label="Sections">
      {headings.map((h, i) => (
        <button
          key={h.id}
          type="button"
          className="moco-rail-item"
          aria-current={i === active}
          aria-label={`Jump to ${h.text}`}
          onClick={() => smoothScrollTo(h.id)}
        >
          <span className="moco-rail-tick" aria-hidden="true" />
          <span className="moco-rail-label" aria-hidden="true">
            {h.text}
          </span>
        </button>
      ))}
    </nav>
  );
}
components/moco/rail.css
.moco-rail {
  position: fixed;
  left: 28px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 0.5rem 0;
}

.moco-rail-item {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  height: 14px;
  /* Button reset the host app may not provide. */
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

.moco-rail-tick {
  display: block;
  height: 1px;
  width: 18px;
  background: var(--moco-muted);
  opacity: 0.4;
  flex: none;
  transition:
    width 300ms var(--moco-ease),
    opacity 300ms var(--moco-ease),
    background 300ms var(--moco-ease);
}

.moco-rail-item[aria-current="true"] .moco-rail-tick {
  width: 30px;
  opacity: 1;
  height: 2px;
  background: var(--moco-accent-dk);
}

.moco-rail-label {
  font-size: 12px;
  color: var(--moco-faint);
  white-space: nowrap;
  opacity: 0;
  transform: translateX(-6px);
  transition:
    opacity 250ms var(--moco-ease),
    transform 250ms var(--moco-ease);
  pointer-events: none;
}

.moco-rail:hover .moco-rail-label,
.moco-rail:focus-within .moco-rail-label {
  opacity: 1;
  transform: none;
}

.moco-rail-item[aria-current="true"] .moco-rail-label {
  color: var(--moco-ink);
}

@media (max-width: 1279px) {
  .moco-rail {
    display: none;
  }
}