Skip to component
Modulo

Snap rail

A draggable rail with velocity-projected detents and continuous spring retargeting.

Preview

Snap rail

Drag to select

Center selected

Controls

Projection time
120 ms
Preview hysteresis
8 px

Flick toward a destination. Grab the item again while it settles.

Installation

bunx shadcn@latest add @modulo/snap-rail

Requires 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 video

Interaction

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

PropTypeDefault
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.stringFirst detent
onSelectionChangeSelection requests. Required with controlled selectedId.(id: string) => voidOptional
onSelectionCommitSuccessful user destination request.(id: string) => voidOptional
onSelectionCancelCanceled pointer interaction without a commit.() => voidOptional
decaySecondsProjection time constant in seconds.number0.12
hysteresisPreview midpoint margin in pixels.number8
omega / dampingRatioIllustrative spring tuning, not Apple constants.number20 / 0.85
disabled / reducedMotionInput availability and still alternate path.booleanfalse
onReadyController on mount and null on cleanup. Keep callback stable.(controller | null) => voidOptional
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...