Preview Rail

A navigation rail whose ticks form a hover pyramid and reveal a floating destination preview.

Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/preview-rail.json
Usage
Pass an `items` array. The floating preview renders through the Card primitive unless you override `renderPreview`.
TSImport
import { PreviewRail, type PreviewRailItem } from "@/components/ui/preview-rail";
TSExample
import { PreviewRail, type PreviewRailItem } from "@/components/ui/preview-rail";

const items: PreviewRailItem[] = [
  // A step can navigate to a URL…
  { id: "overview", label: "Overview", href: "https://thesqd.com", target: "_blank", description: "Opens in a new tab.", badge: "Link", badgeColor: "sky" },
  // …or run any onClick handler (router push, analytics, anything).
  { id: "projects", label: "Projects", onClick: () => console.log("Projects"), description: "Every brief in flight, grouped by status and due date.", badge: "3 active", badgeColor: "brand" },
  { id: "assets", label: "Assets", onClick: () => console.log("Assets"), description: "Delivered files, brand kits, and the reference library.", badge: "New", badgeColor: "emerald" },
  { id: "billing", label: "Billing", onClick: () => console.log("Billing"), description: "Plan, invoices, and usage against your monthly allowance." },
  { id: "settings", label: "Settings", onClick: () => console.log("Settings"), description: "Members, roles, notifications, and workspace defaults.", badge: "Beta", badgeColor: "amber" },
];

export function Example() {
  return <PreviewRail items={items} />;
}
Composition
Anatomy of the PreviewRail component.
PreviewRail                       (single component — pass items as props)
├── items={PreviewRailItem[]}     ({ id, label, href?, onClick?, target?, rel?, description?, badge?, badgeColor?, group? })
│   ├── href="…"                   (navigate to a URL — renders the step as a link)
│   ├── onClick={(e) => …}         (run any handler — renders a button when there's no href)
│   ├── badge={"New" | <Badge/>}   (optional — shown top-right of the preview card)
│   ├── badgeColor="brand"         (BadgeColor token when badge is a string)
│   └── group="Setup"              (bundle consecutive items; subtle gap + label between groups)
├── orientation="vertical"        (vertical | horizontal; default vertical)
├── side="right"                  (left | right — which edge the rail sits on; vertical only)
├── position="left"               (left | right | top | bottom — sets orientation + edge in one prop)
├── inCard={false}                (wrap the rail + children in a <Card> surface)
├── cardClassName="…"             (extend the inCard <Card>; it's sized by the bar count + t-resize tweens changes)
├── sound={true}                  (tick sound on hover via cuelume; set false to mute)
├── variant="rail"                (rail | progress — progress fills the first N=value ticks)
├── value={0}                     (progress only — number of filled ticks)
├── activeId / defaultActiveId    (controlled / uncontrolled selection)
├── onActiveChange={(id) => …}    (fires on click)
├── renderPreview={(item) => …}   (override the floating preview; default is a <Card>)
├── children                      (rendered alongside the rail, fills remaining space)
└── className / railClassName / previewClassName
Vertical
Ticks stack top-to-bottom; hovering scales the focused tick full-width and reveals its Card preview to the right.
Horizontal
Ticks sit in a row; the preview floats above the focused tick.
Progress
`variant="progress"` fills completed stages (index < `value`) in green while keeping the hover pyramid + preview. The current step (index === `value`) is subtly marked via `showPosition`.
Grouped progress
Give items a `group` key to bundle them into phases — a subtle gap plus an uppercase label separates each group on the rail. Shown here as a two-phase onboarding flow.
In a card, on the left
`position="left"` sits the rail on the left edge (the preview reveals to its right) and `inCard` wraps just the rail in a `<Card>` sized by the number of bars. "Grow card" adds steps — the card resizes, tweened by the baked-in `t-resize`.
API Reference
Props exposed by the PreviewRail component.
PropTypeDefaultDescription
itemsPreviewRailItem[]Navigation destinations. Each is `{ id, label, href?, onClick?, target?, rel?, description?, badge?, badgeColor?, group? }`. Provide `href` to navigate (renders a link) and/or `onClick` for any handler (renders a button when there's no href) — both fire `onActiveChange`. `badge` (string or ReactNode) renders top-right of the preview card; `badgeColor` is a `BadgeColor` token used when `badge` is a string.
orientation"vertical" | "horizontal""vertical"Rail direction. Vertical stacks ticks with the preview beside them; horizontal is a row of ticks with the preview floating above.
side"left" | "right""right"Which edge the rail sits on (vertical orientation only). The preview reveals toward the opposite side.
position"left" | "right" | "top" | "bottom"Convenience that sets `orientation` + edge in one prop. `left`/`right` are vertical rails; `top`/`bottom` are horizontal rails (preview floats below / above). Overrides `orientation` and `side` when set.
inCardbooleanfalseWrap the rail in a `<Card>` surface (the rail only — the floating preview and any `children` stay outside so they aren't clipped).
cardClassNamestringExtend the `inCard` `<Card>` classes. The card is auto-sized to the number of bars (rail extent + padding) and `t-resize` is baked in, so the card tweens whenever the item count changes. No effect unless `inCard`.
variant"rail" | "progress""rail"`rail` is the interactive nav with a hover pyramid + preview. `progress` keeps that same hover behavior but fills completed stages (index < `value`) in green, reusing the tick look as a step/progress bar.
valuenumber0`variant="progress"` only — number of leading ticks to fill (0…items.length).
showPositionbooleantrue`variant="progress"` only — subtly mark the current step (the tick at `index === value`, colored `#F2AF39`) so "you are here" reads without competing with the completed/hovered ticks.
showGroupLabelsbooleantrueRender each group's `group` string as a small uppercase label above its first tick. Only has an effect when items define groups.
soundbooleantruePlay a tick sound (via cuelume) on hover of each step. Set `false` to mute. Ships as a registry dependency, so sounds work out of the box.
activeIdstringControlled active item id. Omit for uncontrolled behavior.
defaultActiveIdstringitems[0].idUncontrolled initial active item id.
onActiveChange(id: string) => voidFires when the active item changes (on click).
renderPreview(item: PreviewRailItem) => ReactNodeOverride the floating preview. Defaults to a `<Card>` with the item's label + description.
childrenReactNodeContent rendered alongside the rail; fills the remaining space.
classNamestringExtend the root wrapper classes.
railClassNamestringExtend the `<nav>` rail classes.
previewClassNamestringExtend the floating preview container classes.