A horizontal section driven by vertical scrolling. The frame pins, the strip travels sideways through any content, and a counter reads 01, 1, or your own labels.
How it works
Four steps from first sketch to launch.
Scroll down
Item 1 of 0
pnpm dlx shadcn@latest add @latheui/scroll-railPart of Lathe Pro. Set up your project once with your license key: npx latheui init --key <key>. See Lathe Pro.
Scroll Rail is part of Lathe Pro
The preview is live and free to try. The source, the install and every assembly come with Pro: $79 once, with lifetime updates.
Depends on
Also accepts section props. Anything inside it goes in the pinned frame, beside the track.
| Prop | Type | Default | Description |
|---|---|---|---|
| label* (required) | string | — | Accessible name of the rail. |
| container | RefObject<HTMLElement> | — | The element that scrolls. The page when not set. |
| offset | number | 0 | Pixels covered by something fixed at the top, such as a header. The frame pins below it. |
| height | number | string | — | Height of the pinned frame. Fills the space below offset when not set. |
| speed | number | 1 | Pixels of vertical scroll per pixel of sideways travel. 2 moves the rail half as fast. |
| smooth | boolean | { stiffness, damping, mass } | true | Eases the strip after the scroll on a spring instead of moving in lockstep. |
| snap | boolean | false | Once scrolling stops, settles on the nearest item. |
| pin | "auto" | "always" | "never" | "auto" | auto pins whenever motion is allowed and the rail is at least breakpoint wide, including on phones, and is a swipeable strip otherwise. |
| breakpoint | number | 0 | Width in px below which auto becomes a strip. 0 pins at every width. |
| gap | number | 24 | Space between items in px. |
| onIndexChange | (index: number) => void | — | Called as a new item comes into focus. |
| frameClassName | string | — | Classes for the pinned frame. |
A list item. Put anything in it.
| Prop | Type | Default | Description |
|---|---|---|---|
| width | string | "min(26rem, 78vw)" | Width as any CSS length. Items can differ; none gets wider than the frame. |
| Prop | Type | Default | Description |
|---|---|---|---|
| format | "pad" | "number" | string[] | (index, count) => ReactNode | "pad" | 01 / 05, 1 / 5, one label per item, or your own text. index counts from 0. |
| showTotal | boolean | — | Shows the total after the position. On for "pad" and "number", off otherwise. |
| separator | ReactNode | " / " | Between the position and the total. |
A line that fills from the first item to the last, and one dot per item that scrolls to it when clicked.
| Prop | Type | Default | Description |
|---|---|---|---|
| format | same as the counter | "number" | Dots only: names each dot for assistive tech. |
For your own readouts and controls, inside a ScrollRail.
| Prop | Type | Default | Description |
|---|---|---|---|
| index | number | — | The item in focus, from 0. |
| count | number | — | How many items there are. |
| progress | MotionValue<number> | — | 0 at the first item, 1 at the last. |
| pinned | boolean | — | Whether the page's scroll drives the rail right now. |
| scrollTo | (index: number) => void | — | Scrolls to bring an item into focus. |
<ScrollRail label="Work" offset={64} speed={1.6} snap>
…
</ScrollRail><ScrollRailCounter format={["Intro", "Design", "Build", "Ship"]} />
<ScrollRailCounter format={(i, count) => `Step ${i + 1} of ${count}`} />function Caption({ titles }: { titles: string[] }) {
const { index, scrollTo } = useScrollRail()
return <button onClick={() => scrollTo(index + 1)}>Next: {titles[index + 1]}</button>
}