Preview Rail
NewCodex app-inspired navigation rail with compact ticks that form a hover pyramid and reveal a floating destination preview.
TSXcomponents/previews/motion/preview-rail.preview.tsx
"use client";
import { PreviewRail } from "@/components/motion/preview-rail";
export const previewRailItems = [
{
id: "dashboard",
label: "Dashboard",
description: "Return to your workspace overview and recent activity.",
href: "#dashboard",
},
{
id: "components",
label: "Components",
description: "Browse motion primitives for React and Next.js.",
href: "#components",
},
{
id: "blocks",
label: "Blocks",
description: "Explore composed, product-ready interface blocks.",
href: "#blocks",
},
{
id: "playground",
label: "Playground",
description: "Tune motion values and preview behavior live.",
href: "#playground",
},
{
id: "docs",
label: "Documentation",
description: "Read installation, usage, and API reference notes.",
href: "#docs",
},
{
id: "changelog",
label: "Changelog",
description: "Review newly launched components and improvements.",
href: "#changelog",
},
{
id: "sponsors",
label: "Sponsors",
description: "Support continued development of the open-source library.",
href: "#sponsors",
},
{
id: "pro",
label: "beUI Pro",
description: "Get premium components and lifetime access.",
href: "#pro",
},
{
id: "examples",
label: "Examples",
description: "See components composed in practical interface patterns.",
href: "#examples",
},
{
id: "templates",
label: "Templates",
description: "Start from polished layouts built with beUI components.",
href: "#templates",
},
{
id: "guides",
label: "Guides",
description: "Learn how to combine motion primitives effectively.",
href: "#guides",
},
{
id: "community",
label: "Community",
description: "Discover what other builders are creating with beUI.",
href: "#community",
},
{
id: "github",
label: "GitHub",
description: "View the source, report issues, and contribute improvements.",
href: "#github",
},
{
id: "about",
label: "About",
description: "Learn more about the ideas and people behind beUI.",
href: "#about",
},
];
export function PreviewRailPreview() {
return (
<div className="flex w-full flex-col gap-8">
<PreviewRail
items={previewRailItems}
defaultActiveId="docs"
className="mx-auto h-[360px] w-full max-w-2xl"
/>
<PreviewRail
items={previewRailItems}
orientation="horizontal"
defaultActiveId="docs"
className="mx-auto h-[280px] w-full max-w-2xl"
/>
</div>
);
}
TSXcomponents/motion/preview-rail.tsx
"use client";
// beui.dev/components/motion/preview-rail
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useId, useState, type ReactNode } from "react";
import { EASE_OUT, SPRING_LAYOUT } from "@/lib/ease";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import { cn } from "@/lib/utils";
export interface PreviewRailItem {
id: string;
label: string;
description?: ReactNode;
href: string;
target?: "_blank" | "_self" | "_parent" | "_top";
rel?: string;
}
export interface PreviewRailProps {
items: PreviewRailItem[];
orientation?: "vertical" | "horizontal";
activeId?: string;
defaultActiveId?: string;
onActiveChange?: (id: string) => void;
renderPreview?: (item: PreviewRailItem) => ReactNode;
children?: ReactNode;
className?: string;
railClassName?: string;
previewClassName?: string;
}
function DefaultPreview({ item }: { item: PreviewRailItem }) {
return (
<div className="rounded-2xl border border-border bg-card p-4 shadow-sm">
<p className="font-medium text-card-foreground">{item.label}</p>
{item.description ? (
<div className="mt-1 text-sm leading-6 text-muted-foreground">
{item.description}
</div>
) : null}
</div>
);
}
export function PreviewRail({
items,
orientation = "vertical",
activeId,
defaultActiveId,
onActiveChange,
renderPreview,
children,
className,
railClassName,
previewClassName,
}: PreviewRailProps) {
const uid = useId();
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const [internalActiveId, setInternalActiveId] = useState(
defaultActiveId ?? items[0]?.id ?? "",
);
const [hoveredId, setHoveredId] = useState<string | null>(null);
const [focusedId, setFocusedId] = useState<string | null>(null);
const requestedActiveId = activeId ?? internalActiveId;
const selectedId = items.some((item) => item.id === requestedActiveId)
? requestedActiveId
: (items[0]?.id ?? "");
const displayedId = hoveredId ?? focusedId ?? "";
const displayedIndex = items.findIndex((item) => item.id === displayedId);
const rowTemplate = items.length
? `repeat(${items.length}, 1.25rem)`
: undefined;
const isHorizontal = orientation === "horizontal";
const selectItem = (id: string) => {
if (activeId === undefined) setInternalActiveId(id);
onActiveChange?.(id);
};
return (
<motion.div
layoutRoot
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) {
setFocusedId(null);
}
}}
className={cn(
"isolate relative flex w-full overflow-visible",
isHorizontal
? "min-h-64 flex-col items-center justify-center"
: "min-h-80",
className,
)}
>
<nav
aria-label="Section navigation"
onPointerLeave={() => setHoveredId(null)}
style={
isHorizontal
? { gridTemplateColumns: rowTemplate }
: { gridTemplateRows: rowTemplate }
}
className={cn(
"relative z-10 grid shrink-0",
isHorizontal
? "h-12 w-fit max-w-full self-center justify-center"
: "w-12 content-center",
railClassName,
)}
>
{items.map((item, index) => {
const selected = item.id === selectedId;
const displayed = item.id === displayedId;
const highlighted = displayed;
const distance =
displayedIndex < 0 ? Number.POSITIVE_INFINITY : Math.abs(index - displayedIndex);
const scale = highlighted
? 1
: distance === 1
? 0.68
: distance === 2
? 0.44
: 0.25;
return (
<a
key={item.id}
href={item.href}
target={item.target}
rel={
item.rel ??
(item.target === "_blank" ? "noreferrer noopener" : undefined)
}
aria-label={item.label}
aria-current={selected ? "page" : undefined}
onPointerEnter={() => {
if (canHover) setHoveredId(item.id);
}}
onPointerDown={() => setFocusedId(null)}
onFocus={(event) => {
if (event.currentTarget.matches(":focus-visible")) {
setFocusedId(item.id);
}
}}
onClick={() => selectItem(item.id)}
className={cn(
"relative flex text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
isHorizontal
? "h-12 w-5 items-end justify-center"
: "h-5 w-12 items-center",
)}
>
<motion.span
aria-hidden="true"
animate={isHorizontal ? { scaleY: scale } : { scaleX: scale }}
transition={reduce ? { duration: 0 } : SPRING_LAYOUT}
className={cn(
"block bg-current",
isHorizontal
? "h-12 w-0.5 origin-bottom"
: "h-0.5 w-12 origin-left",
highlighted ? "text-foreground" : undefined,
)}
/>
</a>
);
})}
</nav>
<div
aria-hidden="true"
style={
isHorizontal
? { gridTemplateColumns: rowTemplate }
: { gridTemplateRows: rowTemplate }
}
className={cn(
"pointer-events-none absolute z-50 grid",
isHorizontal
? "top-1/2 left-1/2 h-5 w-fit max-w-full -translate-x-1/2 -translate-y-1/2 justify-center"
: "inset-y-0 right-4 left-16 content-center",
)}
>
{items.map((item) => (
<div
key={item.id}
className={cn(
"relative flex h-5 items-center",
isHorizontal ? "w-5 justify-center" : undefined,
)}
>
{item.id === displayedId ? (
<div
className={cn(
isHorizontal
? "absolute bottom-12 left-1/2 w-72 -translate-x-1/2"
: "w-full max-w-sm",
previewClassName,
)}
>
<motion.div
layoutId={`preview-rail-card-${uid}`}
transition={reduce ? { duration: 0 } : SPRING_LAYOUT}
>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={item.id}
initial={
reduce
? { opacity: 0 }
: { opacity: 0, y: 4, filter: "blur(6px)" }
}
animate={
reduce
? { opacity: 1 }
: { opacity: 1, y: 0, filter: "blur(0px)" }
}
exit={
reduce
? { opacity: 0 }
: {
opacity: 0,
y: -2,
filter: "blur(4px)",
transition: {
duration: 0.12,
ease: EASE_OUT,
},
}
}
transition={{
duration: reduce ? 0 : 0.18,
ease: EASE_OUT,
}}
>
{renderPreview ? (
renderPreview(item)
) : (
<DefaultPreview item={item} />
)}
</motion.div>
</AnimatePresence>
</motion.div>
</div>
) : null}
</div>
))}
</div>
{children ? (
<div className="min-h-0 min-w-0 flex-1">{children}</div>
) : null}
</motion.div>
);
}
Install
Add it with the shadcn CLI, or copy the source manually.
$ bunx --bun shadcn add @beui/preview-rail
Needs the theme tokens once. Already ran
shadcn init? You are set. Theme setupInstall dependencies
npm i clsx motion tailwind-mergeAdd util files
TSXlib/ease.ts
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.
export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.6,
} as const;
/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
stiffness: 460,
damping: 30,
mass: 0.55,
} as const;
/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
stiffness: 420,
damping: 40,
mass: 0.5,
} as const;
/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
type: "spring",
stiffness: 360,
damping: 32,
mass: 0.6,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
stiffness: 200,
damping: 15,
mass: 0.3,
} as const;
TSXlib/hooks/use-hover-capable.ts
"use client";
import { useEffect, useState } from "react";
/**
* Returns true only on devices that have a true hover (mouse / trackpad).
* Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere
* — gate hover-only effects (scale lifts, magnetic pulls) behind this.
*/
export function useHoverCapable() {
const [canHover, setCanHover] = useState(false);
useEffect(() => {
if (typeof window === "undefined" || !window.matchMedia) return;
const mq = window.matchMedia("(hover: hover) and (pointer: fine)");
const update = () => setCanHover(mq.matches);
update();
mq.addEventListener?.("change", update);
return () => mq.removeEventListener?.("change", update);
}, []);
return canHover;
}
TSXlib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy the source code
TSXcomponents/motion/preview-rail.tsx
"use client";
// beui.dev/components/motion/preview-rail
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useId, useState, type ReactNode } from "react";
import { EASE_OUT, SPRING_LAYOUT } from "@/lib/ease";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import { cn } from "@/lib/utils";
export interface PreviewRailItem {
id: string;
label: string;
description?: ReactNode;
href: string;
target?: "_blank" | "_self" | "_parent" | "_top";
rel?: string;
}
export interface PreviewRailProps {
items: PreviewRailItem[];
orientation?: "vertical" | "horizontal";
activeId?: string;
defaultActiveId?: string;
onActiveChange?: (id: string) => void;
renderPreview?: (item: PreviewRailItem) => ReactNode;
children?: ReactNode;
className?: string;
railClassName?: string;
previewClassName?: string;
}
function DefaultPreview({ item }: { item: PreviewRailItem }) {
return (
<div className="rounded-2xl border border-border bg-card p-4 shadow-sm">
<p className="font-medium text-card-foreground">{item.label}</p>
{item.description ? (
<div className="mt-1 text-sm leading-6 text-muted-foreground">
{item.description}
</div>
) : null}
</div>
);
}
export function PreviewRail({
items,
orientation = "vertical",
activeId,
defaultActiveId,
onActiveChange,
renderPreview,
children,
className,
railClassName,
previewClassName,
}: PreviewRailProps) {
const uid = useId();
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const [internalActiveId, setInternalActiveId] = useState(
defaultActiveId ?? items[0]?.id ?? "",
);
const [hoveredId, setHoveredId] = useState<string | null>(null);
const [focusedId, setFocusedId] = useState<string | null>(null);
const requestedActiveId = activeId ?? internalActiveId;
const selectedId = items.some((item) => item.id === requestedActiveId)
? requestedActiveId
: (items[0]?.id ?? "");
const displayedId = hoveredId ?? focusedId ?? "";
const displayedIndex = items.findIndex((item) => item.id === displayedId);
const rowTemplate = items.length
? `repeat(${items.length}, 1.25rem)`
: undefined;
const isHorizontal = orientation === "horizontal";
const selectItem = (id: string) => {
if (activeId === undefined) setInternalActiveId(id);
onActiveChange?.(id);
};
return (
<motion.div
layoutRoot
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) {
setFocusedId(null);
}
}}
className={cn(
"isolate relative flex w-full overflow-visible",
isHorizontal
? "min-h-64 flex-col items-center justify-center"
: "min-h-80",
className,
)}
>
<nav
aria-label="Section navigation"
onPointerLeave={() => setHoveredId(null)}
style={
isHorizontal
? { gridTemplateColumns: rowTemplate }
: { gridTemplateRows: rowTemplate }
}
className={cn(
"relative z-10 grid shrink-0",
isHorizontal
? "h-12 w-fit max-w-full self-center justify-center"
: "w-12 content-center",
railClassName,
)}
>
{items.map((item, index) => {
const selected = item.id === selectedId;
const displayed = item.id === displayedId;
const highlighted = displayed;
const distance =
displayedIndex < 0 ? Number.POSITIVE_INFINITY : Math.abs(index - displayedIndex);
const scale = highlighted
? 1
: distance === 1
? 0.68
: distance === 2
? 0.44
: 0.25;
return (
<a
key={item.id}
href={item.href}
target={item.target}
rel={
item.rel ??
(item.target === "_blank" ? "noreferrer noopener" : undefined)
}
aria-label={item.label}
aria-current={selected ? "page" : undefined}
onPointerEnter={() => {
if (canHover) setHoveredId(item.id);
}}
onPointerDown={() => setFocusedId(null)}
onFocus={(event) => {
if (event.currentTarget.matches(":focus-visible")) {
setFocusedId(item.id);
}
}}
onClick={() => selectItem(item.id)}
className={cn(
"relative flex text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
isHorizontal
? "h-12 w-5 items-end justify-center"
: "h-5 w-12 items-center",
)}
>
<motion.span
aria-hidden="true"
animate={isHorizontal ? { scaleY: scale } : { scaleX: scale }}
transition={reduce ? { duration: 0 } : SPRING_LAYOUT}
className={cn(
"block bg-current",
isHorizontal
? "h-12 w-0.5 origin-bottom"
: "h-0.5 w-12 origin-left",
highlighted ? "text-foreground" : undefined,
)}
/>
</a>
);
})}
</nav>
<div
aria-hidden="true"
style={
isHorizontal
? { gridTemplateColumns: rowTemplate }
: { gridTemplateRows: rowTemplate }
}
className={cn(
"pointer-events-none absolute z-50 grid",
isHorizontal
? "top-1/2 left-1/2 h-5 w-fit max-w-full -translate-x-1/2 -translate-y-1/2 justify-center"
: "inset-y-0 right-4 left-16 content-center",
)}
>
{items.map((item) => (
<div
key={item.id}
className={cn(
"relative flex h-5 items-center",
isHorizontal ? "w-5 justify-center" : undefined,
)}
>
{item.id === displayedId ? (
<div
className={cn(
isHorizontal
? "absolute bottom-12 left-1/2 w-72 -translate-x-1/2"
: "w-full max-w-sm",
previewClassName,
)}
>
<motion.div
layoutId={`preview-rail-card-${uid}`}
transition={reduce ? { duration: 0 } : SPRING_LAYOUT}
>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={item.id}
initial={
reduce
? { opacity: 0 }
: { opacity: 0, y: 4, filter: "blur(6px)" }
}
animate={
reduce
? { opacity: 1 }
: { opacity: 1, y: 0, filter: "blur(0px)" }
}
exit={
reduce
? { opacity: 0 }
: {
opacity: 0,
y: -2,
filter: "blur(4px)",
transition: {
duration: 0.12,
ease: EASE_OUT,
},
}
}
transition={{
duration: reduce ? 0 : 0.18,
ease: EASE_OUT,
}}
>
{renderPreview ? (
renderPreview(item)
) : (
<DefaultPreview item={item} />
)}
</motion.div>
</AnimatePresence>
</motion.div>
</div>
) : null}
</div>
))}
</div>
{children ? (
<div className="min-h-0 min-w-0 flex-1">{children}</div>
) : null}
</motion.div>
);
}
API Reference
items{}—orientation?"vertical" | "horizontal"verticalactiveId?string—defaultActiveId?string—onActiveChange?((id: string) => void)—renderPreview?((item: PreviewRailItem) => ReactNode)—className?string—railClassName?string—previewClassName?string—Keep in mind
Some components on this site are inspired by or recreated from existing work across the web. I'm not here to take credit; just to learn, experiment, and sometimes push things a bit further. If something looks familiar and I forgot to mention you, reach out and I'll fix that right away.