A navigation rail whose ticks form a hover pyramid and reveal a floating destination preview.
npx shadcn@latest add https://sdk-components.thesqd.com/r/preview-rail.jsonimport { PreviewRail, type PreviewRailItem } from "@/components/ui/preview-rail";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} />;
}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| Prop | Type | Default | Description |
|---|---|---|---|
items | PreviewRailItem[] | — | 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. |
inCard | boolean | false | Wrap the rail in a `<Card>` surface (the rail only — the floating preview and any `children` stay outside so they aren't clipped). |
cardClassName | string | — | Extend 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. |
value | number | 0 | `variant="progress"` only — number of leading ticks to fill (0…items.length). |
showPosition | boolean | true | `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. |
showGroupLabels | boolean | true | Render each group's `group` string as a small uppercase label above its first tick. Only has an effect when items define groups. |
sound | boolean | true | Play 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. |
activeId | string | — | Controlled active item id. Omit for uncontrolled behavior. |
defaultActiveId | string | items[0].id | Uncontrolled initial active item id. |
onActiveChange | (id: string) => void | — | Fires when the active item changes (on click). |
renderPreview | (item: PreviewRailItem) => ReactNode | — | Override the floating preview. Defaults to a `<Card>` with the item's label + description. |
children | ReactNode | — | Content rendered alongside the rail; fills the remaining space. |
className | string | — | Extend the root wrapper classes. |
railClassName | string | — | Extend the `<nav>` rail classes. |
previewClassName | string | — | Extend the floating preview container classes. |