KQ-2104 · DOUBLE/RAIL
Double Rail
An app frame with two tiers of navigation: a slim rail of sections whose pill slides on springs.snap, and a panel listing what is inside the chosen one. Changing section slides the panel's list sideways on springs.glide in the direction of travel while its title and count carry over as shared elements — the title cross-fades in place, the count glides to sit after it and rolls to its new number on snap; on a phone the section row's title and count fly into the drilled header. Up and Down move within a tier, Right crosses from rail to panel, Left comes back.
KQ-2104 · DOUBLE/RAIL
ML
Fleet
gaugeworks · fleet · vans · 24
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/double-rail
Props
- sections
- RailSection[]
- default defaultRailSections
- Sections for the rail, each with its icon, count and grouped secondary list; loading and empty sections are states, not errors.
- section / defaultSection
- string
- default — / the first section
- The current section id, controlled or uncontrolled.
- onSection
- (id: string) => void
- default —
- Fires from the rail (click or arrows) with the section chosen.
- page / defaultPage
- string
- default — / the first item
- The current page: an item id, unique across sections.
- onNavigate
- (sectionId: string, itemId: string) => void
- default —
- Fires whenever the current page changes, including when a section change brings its remembered page.
- collapsed / defaultCollapsed
- boolean
- default — / false
- Whether the panel is folded away, controlled or uncontrolled.
- onCollapse
- (collapsed: boolean) => void
- default —
- Fires from the toggle, or when choosing a section reopens the panel.
- onEmptyAction
- (sectionId: string) => void
- default —
- An empty section's action button was pressed.
- panelWidth
- number
- default 220
- Width of the secondary panel, px.
- slideDirection
- "sideways" | "vertical" | "fade"
- default "sideways"
- How the panel's list moves when the section changes: sideways in the direction of travel, vertically with the rail, or a cross-fade.
- accent
- string
- default "var(--accent-bright)"
- The rail pill, the current page and counts in the current section — any CSS colour.
- renderPage
- (section: RailSection, item: RailItem | null) => ReactNode
- default —
- Draws the page in place of the built-in one.
- product
- string
- default "Gaugeworks"
- The product's name, on the rail's mark and in the phone header.
- user
- string
- default "Mara Lindqvist"
- Who is signed in, for the rail's avatar.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the navigation has arrived.
- onRetry
- () => void
- default —
- Try again was pressed after the navigation failed to load.
- height
- number
- default 520
- The frame's height, px.
- className
- string
- default —
- Classes for the outer frame.
| Prop | Type | Default | Description |
|---|---|---|---|
| sections | RailSection[] | defaultRailSections | Sections for the rail, each with its icon, count and grouped secondary list; loading and empty sections are states, not errors. |
| section / defaultSection | string | — / the first section | The current section id, controlled or uncontrolled. |
| onSection | (id: string) => void | — | Fires from the rail (click or arrows) with the section chosen. |
| page / defaultPage | string | — / the first item | The current page: an item id, unique across sections. |
| onNavigate | (sectionId: string, itemId: string) => void | — | Fires whenever the current page changes, including when a section change brings its remembered page. |
| collapsed / defaultCollapsed | boolean | — / false | Whether the panel is folded away, controlled or uncontrolled. |
| onCollapse | (collapsed: boolean) => void | — | Fires from the toggle, or when choosing a section reopens the panel. |
| onEmptyAction | (sectionId: string) => void | — | An empty section's action button was pressed. |
| panelWidth | number | 220 | Width of the secondary panel, px. |
| slideDirection | "sideways" | "vertical" | "fade" | "sideways" | How the panel's list moves when the section changes: sideways in the direction of travel, vertically with the rail, or a cross-fade. |
| accent | string | "var(--accent-bright)" | The rail pill, the current page and counts in the current section — any CSS colour. |
| renderPage | (section: RailSection, item: RailItem | null) => ReactNode | — | Draws the page in place of the built-in one. |
| product | string | "Gaugeworks" | The product's name, on the rail's mark and in the phone header. |
| user | string | "Mara Lindqvist" | Who is signed in, for the rail's avatar. |
| status | "ready" | "loading" | "error" | "ready" | Whether the navigation has arrived. |
| onRetry | () => void | — | Try again was pressed after the navigation failed to load. |
| height | number | 520 | The frame's height, px. |
| className | string | — | Classes for the outer frame. |