Snap rail
A draggable rail with velocity-projected detents and continuous spring retargeting.
Preview
Snap rail
Drag to selectCenter selected
Controls
Flick toward a destination. Grab the item again while it settles.
Installation
bunx shadcn@latest add @modulo/snap-railRequires an active Modulo key and a project configured for shadcn.
Registry setup
Set MODULO_REGISTRY_KEY to your key. Add this registry entry to components.json.
{
"registries": {
"@modulo": {
"url": "<registry-url>/r/{name}.json",
"headers": {
"Authorization": "Bearer ${MODULO_REGISTRY_KEY}"
}
}
}
}Usage
import { SnapRail } from "@/components/snap-rail/SnapRail";
const detents = [
{ id: "left", label: "Left", position: 0 },
{ id: "center", label: "Center", position: 140 },
{ id: "right", label: "Right", position: 280 },
];
export default function Example() {
return <SnapRail ariaLabel="Placement" detents={detents} defaultSelectedId="center" />;
}React and browser Pointer Events. All four source files install together. No animation package required.
Registry JSONDownload thumbnailDownload preview GIFDownload preview videoInteraction
Drag the item horizontally. Release speed projects a destination, then the item springs to the nearest detent. Grab it again to interrupt. A hold before release discards stale flick speed.
Use the destination buttons, arrow keys, Home or End. Labels stay rigid. Cancellation returns to the selected detent without committing. Reduced motion selects the destination immediately. Hidden and offscreen previews stop animation.
The spring tuning and projection are original examples. They are not Apple implementation constants.
API
| Prop | Type | Default |
|---|---|---|
detentsStable IDs, labels, and increasing finite relative positions mapped across the track. | readonly SnapDetent[] | Required |
ariaLabelAccessible name of the placement control. | string | "Snap position" |
selectedId / defaultSelectedIdControlled selection or initial local selection. | string | First detent |
onSelectionChangeSelection requests. Required with controlled selectedId. | (id: string) => void | Optional |
onSelectionCommitSuccessful user destination request. | (id: string) => void | Optional |
onSelectionCancelCanceled pointer interaction without a commit. | () => void | Optional |
decaySecondsProjection time constant in seconds. | number | 0.12 |
hysteresisPreview midpoint margin in pixels. | number | 8 |
omega / dampingRatioIllustrative spring tuning, not Apple constants. | number | 20 / 0.85 |
disabled / reducedMotionInput availability and still alternate path. | boolean | false |
onReadyController on mount and null on cleanup. Keep callback stable. | (controller | null) => void | Optional |
Selection and controller
Detents have stable IDs, labels and strictly increasing finite positions. One detent is supported. Pass either selectedId with onSelectionChange, or defaultSelectedId. The parent owns a controlled selection.
onSelectionCommit(id) fires once after a successful pointer release or keyboard destination request. onSelectionCancel() reports an interrupted gesture. Controller selections and replays emit changes without user commits.
select(id) retargets from the current position and velocity. replay() runs a finite sequence. reset() restores the first uncontrolled selection, or the current controlled selection. snapshot() exposes geometry, velocity, preview, lifecycle and event counts. The component calls destroy() on unmount.
Source
Checking source access...