KQ-1281 · PANE/STACK
Pane Stack
Nested navigation that keeps the way back in view: opening a row slides its pane in from the right on springs.glide, and the pane it covers is pushed and compresses into a narrow labelled tab along the left edge, its content clipped rather than squashed while the tab face takes over. One motion value is the stack's position, so every pane edge, the staircase of covered tabs and the fold of tabs that no longer fit (with a count) are functions of it; a tab, Escape or a 1:1 swipe of the open pane to the right goes back, the throw projected and carried on glide with its release velocity. Rows are a list with one tab stop (arrows move, Enter, Space or ArrowRight open, ArrowLeft goes back), the tabs are a nav with their own stop, and focus moves into each new pane and back onto the row you came from.
Plots
Measured this season
Plots · level 4 · 3 tabs back
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/pane-stack
Props
- root
- PaneStackNode
- default defaultPaneTree
- The tree to navigate: each node has an id, title, optional description, meta, icon, content and children. defaultPaneTree is a Fieldline workspace seven levels deep.
- value / defaultValue
- string
- default the root's id
- The open pane's id, controlled or initial.
- onValueChange
- (id: string, path: string[]) => void
- default —
- Fires from the press, key or swipe that asked to open a pane, with its id and the ids from the root to it.
- onSelect
- (id: string) => void
- default —
- A row with nothing inside it (no children, no content) was chosen.
- label
- string
- default "Navigation"
- The container's accessible name.
- depth
- number
- default 0.5
- How far covered panes recede, 0 to 1: tabs step in from top and bottom, shade toward the page and take the open pane's shadow. 0 is flat.
- compress
- number
- default 40
- Width of a covered pane's tab in px (28 to 64); from 36 up the tab shows its level's icon.
- speed
- number
- default 1
- Tempo of every slide, 0.5 to 1.5: the glide spring sped up or slowed down with its damping ratio kept.
- minPaneWidth
- number
- default 200
- The narrowest the open pane gets, in px; past it the oldest tabs fold under the first, which carries a count.
- swipeBack
- boolean
- default true
- Drag the open pane to the right to go back a level.
- backLabel
- (title: string) => string
- default (title) => `Back to ${title}`
- A tab's accessible name, from its pane's title.
- emptyLabel
- string
- default "Nothing in here yet"
- What a pane with no content and no children says.
- height
- number | string
- default 440
- The container's height, in px or any CSS length.
- size
- "sm" | "md" | "lg"
- default "md"
- Row height 36, 44 or 52 px; small rows drop the description line.
- accent
- string
- default "var(--accent-bright)"
- The trail back (the row you came from), the chosen row and the fold badge; any CSS colour.
- sound
- boolean
- default false
- A swish for each push and pop, a click for a tab, a chosen row and the swipe's half-way detent.
- disabled
- boolean
- default false
- Inert and dimmed: no presses, keys or swipes.
- className
- string
- default —
- Classes for the container.
| Prop | Type | Default | Description |
|---|---|---|---|
| root | PaneStackNode | defaultPaneTree | The tree to navigate: each node has an id, title, optional description, meta, icon, content and children. defaultPaneTree is a Fieldline workspace seven levels deep. |
| value / defaultValue | string | the root's id | The open pane's id, controlled or initial. |
| onValueChange | (id: string, path: string[]) => void | — | Fires from the press, key or swipe that asked to open a pane, with its id and the ids from the root to it. |
| onSelect | (id: string) => void | — | A row with nothing inside it (no children, no content) was chosen. |
| label | string | "Navigation" | The container's accessible name. |
| depth | number | 0.5 | How far covered panes recede, 0 to 1: tabs step in from top and bottom, shade toward the page and take the open pane's shadow. 0 is flat. |
| compress | number | 40 | Width of a covered pane's tab in px (28 to 64); from 36 up the tab shows its level's icon. |
| speed | number | 1 | Tempo of every slide, 0.5 to 1.5: the glide spring sped up or slowed down with its damping ratio kept. |
| minPaneWidth | number | 200 | The narrowest the open pane gets, in px; past it the oldest tabs fold under the first, which carries a count. |
| swipeBack | boolean | true | Drag the open pane to the right to go back a level. |
| backLabel | (title: string) => string | (title) => `Back to ${title}` | A tab's accessible name, from its pane's title. |
| emptyLabel | string | "Nothing in here yet" | What a pane with no content and no children says. |
| height | number | string | 440 | The container's height, in px or any CSS length. |
| size | "sm" | "md" | "lg" | "md" | Row height 36, 44 or 52 px; small rows drop the description line. |
| accent | string | "var(--accent-bright)" | The trail back (the row you came from), the chosen row and the fold badge; any CSS colour. |
| sound | boolean | false | A swish for each push and pop, a click for a tab, a chosen row and the swipe's half-way detent. |
| disabled | boolean | false | Inert and dimmed: no presses, keys or swipes. |
| className | string | — | Classes for the container. |