KQ-1286 · PUSH/SHEET
Push Sheet
A bottom sheet that pushes its page back: as the sheet rises on springs.glide, the page behind scales down about its top centre, drops a few pixels, rounds its corners and dims like a card behind another in a stack, and a sheet opened from a sheet steps the one beneath it back the same way. Every layer's look is a function of how far the sheets above it have risen, so the grabber and title bar drag the top sheet 1:1 with everything behind following, rubber-banded past the top stop, and a release projects the throw to the nearest stop (half and full by default) carried on glide with its velocity, or sends the sheet away at its own speed. Each sheet is a modal dialog with focus moved in and trapped, Escape closes it and returns focus to its opener, and its grabber is a slider whose arrows move between stops.
nothing open · press send
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/push-sheet
Props
- sheets
- PushSheetItem[]
- default —
- Every sheet that can open: id, title, description, content (a node or a function of the stack's controls) and optional snaps of its own.
- children
- React.ReactNode | ((api: PushSheetApi) => React.ReactNode)
- default —
- The page under the sheets. As a function it receives { open, close, snapTo, stack }.
- value / defaultValue
- string[]
- default []
- The open sheets' ids, bottom first, controlled or initial (at most four).
- onValueChange
- (stack: string[]) => void
- default —
- Fires from the call, drag, key or button that opened or closed a sheet, with the new stack.
- onSnapChange
- (id: string, snap: number) => void
- default —
- A sheet came to rest on a stop after a drag, a key or snapTo, given as a share of its full height.
- push
- number
- default 0.6
- How far a covered layer steps back, 0 to 1: 0 only dims it; 1 shrinks it 6% per level, drops it 16px and rounds it fully.
- snaps
- "half-full" | "full" | "thirds" | number[]
- default "half-full"
- Where a sheet stops: half and full height, full only, thirds, or your own shares of the full height.
- dim
- number
- default 0.35
- How dark a fully covered layer gets, 0 to 0.8; a second level darkens it by half again.
- openAt
- "lowest" | "highest"
- default "lowest"
- Where a new sheet comes to rest.
- inset
- number
- default 28
- The gap above a sheet at full height, in px, where the layer behind peeks out.
- width
- number
- default 560
- A sheet's widest, in px; it is centred when the box is wider.
- radius
- number
- default 16
- The sheets' top corners and the pushed page's corners, in px.
- dismissible
- boolean
- default true
- Whether a drag down, Escape, a press on the page behind and ArrowDown at the lowest stop close the top sheet.
- height
- number | string
- default 520
- The box's height, in px or any CSS length; everything stays inside it.
- backdrop
- string
- default "color-mix(in oklab, var(--background) 45%, black)"
- Any CSS colour shown behind the pushed page.
- closeLabel
- string
- default "Close"
- The close button's accessible name.
- sound
- boolean
- default false
- A swish as a sheet opens or is sent away, and a thock when it lands on a stop, louder for a harder throw and lower for a higher stop.
- disabled
- boolean
- default false
- Ignores opens, drags and keys and dims the box.
- className
- string
- default —
- Classes for the root element.
| Prop | Type | Default | Description |
|---|---|---|---|
| sheets | PushSheetItem[] | — | Every sheet that can open: id, title, description, content (a node or a function of the stack's controls) and optional snaps of its own. |
| children | React.ReactNode | ((api: PushSheetApi) => React.ReactNode) | — | The page under the sheets. As a function it receives { open, close, snapTo, stack }. |
| value / defaultValue | string[] | [] | The open sheets' ids, bottom first, controlled or initial (at most four). |
| onValueChange | (stack: string[]) => void | — | Fires from the call, drag, key or button that opened or closed a sheet, with the new stack. |
| onSnapChange | (id: string, snap: number) => void | — | A sheet came to rest on a stop after a drag, a key or snapTo, given as a share of its full height. |
| push | number | 0.6 | How far a covered layer steps back, 0 to 1: 0 only dims it; 1 shrinks it 6% per level, drops it 16px and rounds it fully. |
| snaps | "half-full" | "full" | "thirds" | number[] | "half-full" | Where a sheet stops: half and full height, full only, thirds, or your own shares of the full height. |
| dim | number | 0.35 | How dark a fully covered layer gets, 0 to 0.8; a second level darkens it by half again. |
| openAt | "lowest" | "highest" | "lowest" | Where a new sheet comes to rest. |
| inset | number | 28 | The gap above a sheet at full height, in px, where the layer behind peeks out. |
| width | number | 560 | A sheet's widest, in px; it is centred when the box is wider. |
| radius | number | 16 | The sheets' top corners and the pushed page's corners, in px. |
| dismissible | boolean | true | Whether a drag down, Escape, a press on the page behind and ArrowDown at the lowest stop close the top sheet. |
| height | number | string | 520 | The box's height, in px or any CSS length; everything stays inside it. |
| backdrop | string | "color-mix(in oklab, var(--background) 45%, black)" | Any CSS colour shown behind the pushed page. |
| closeLabel | string | "Close" | The close button's accessible name. |
| sound | boolean | false | A swish as a sheet opens or is sent away, and a thock when it lands on a stop, louder for a harder throw and lower for a higher stop. |
| disabled | boolean | false | Ignores opens, drags and keys and dims the box. |
| className | string | — | Classes for the root element. |