KQ-1124 · STORY/CUBE
Story Cube
A stories viewer on a real CSS 3D cube inside its own phone-shaped screen: a tap on the right steps forward and on the left steps back, a held press pauses, and the bars fill and advance on their own. A sideways swipe turns the cube to the next person 1:1 under the finger, projects the throw and lands on the glide spring with the release velocity, each face darkening as it turns away; a downward swipe lifts and shrinks the frame, and let go far enough it folds into a circle and glides back into that person's avatar in the tray, otherwise it snaps back. The viewer is a focusable group whose arrow keys step, Shift with them turns, Space pauses and Escape closes, with the same tick per story and whoosh per turn.
Waylight · Trips
Up before the sun on Coldbrook ridge.
Tap the right of the picture for the next story and the left for the previous one, and hold to pause. Swipe sideways to turn to another person and down to close. Left and Right step through stories, Shift with Left or Right turns to the previous or next person, Space pauses and Escape closes.
watching mara · 1 of 3 · tap or swipe
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/story-cube
Props
- people
- StoryCubePerson[]
- default —
- Everyone with stories, in the order the cube turns through them: { id, name, posted?, stories: { id, label, content, caption? }[] }. A story's label is its picture's accessible name; content is the picture.
- value / defaultValue
- { person: string; story: number }
- default { person: <first id>, story: 0 }
- Whose story is on show, by person id, and which of theirs. Controlled or uncontrolled; the cube turns when the person changes, whoever changed it.
- onValueChange
- (value: StoryCubeValue) => void
- default —
- Fires from the tap, swipe, key or autoplay that moved it, with the new position.
- open / defaultOpen
- boolean
- default false
- Whether the viewer is open over the tray. Opening grows it out of the person's avatar; closing folds it back in. A refused close springs back.
- onOpenChange
- (open: boolean) => void
- default —
- Fires from the swipe, key, button, tray face or end of the last story that opened or closed the viewer.
- duration
- number
- default 5
- Seconds each story plays before the next one, 2 to 10.
- depth
- number
- default 0.5
- How deep the cube reads when it turns, 0 to 1: the perspective runs from four frame widths (almost flat) to 1.1 (steep), and the turning face darkens more.
- bars
- "split" | "single" | "ring"
- default "split"
- How progress is drawn: a bar per story, one bar notched at each story, or arcs around the avatar in the header.
- autoplay
- boolean
- default true
- Stories play on their own and advance when their bar is full. Off, each waits for a tap and its bar shows full to mark the place.
- label
- string
- default "Stories"
- The tray's name, shown above it and used as its accessible name.
- children
- ReactNode
- default —
- The screen under the tray: the host's feed, shown while the viewer is closed.
- sound
- boolean
- default false
- A tick for every story a tap, key or open starts, climbing the scale, and a whoosh when a swipe, key or tap turns the cube (a low one when it closes). Automatic advances are silent. Nothing plays, and no audio context exists, unless this is on.
- disabled
- boolean
- default false
- Dims the screen, stops the bars, and ignores taps, swipes and keys.
- className
- string
- default —
- Merged onto the screen's classes; the screen is w-full at 9:16 unless an aspect class here says otherwise.
| Prop | Type | Default | Description |
|---|---|---|---|
| people | StoryCubePerson[] | — | Everyone with stories, in the order the cube turns through them: { id, name, posted?, stories: { id, label, content, caption? }[] }. A story's label is its picture's accessible name; content is the picture. |
| value / defaultValue | { person: string; story: number } | { person: <first id>, story: 0 } | Whose story is on show, by person id, and which of theirs. Controlled or uncontrolled; the cube turns when the person changes, whoever changed it. |
| onValueChange | (value: StoryCubeValue) => void | — | Fires from the tap, swipe, key or autoplay that moved it, with the new position. |
| open / defaultOpen | boolean | false | Whether the viewer is open over the tray. Opening grows it out of the person's avatar; closing folds it back in. A refused close springs back. |
| onOpenChange | (open: boolean) => void | — | Fires from the swipe, key, button, tray face or end of the last story that opened or closed the viewer. |
| duration | number | 5 | Seconds each story plays before the next one, 2 to 10. |
| depth | number | 0.5 | How deep the cube reads when it turns, 0 to 1: the perspective runs from four frame widths (almost flat) to 1.1 (steep), and the turning face darkens more. |
| bars | "split" | "single" | "ring" | "split" | How progress is drawn: a bar per story, one bar notched at each story, or arcs around the avatar in the header. |
| autoplay | boolean | true | Stories play on their own and advance when their bar is full. Off, each waits for a tap and its bar shows full to mark the place. |
| label | string | "Stories" | The tray's name, shown above it and used as its accessible name. |
| children | ReactNode | — | The screen under the tray: the host's feed, shown while the viewer is closed. |
| sound | boolean | false | A tick for every story a tap, key or open starts, climbing the scale, and a whoosh when a swipe, key or tap turns the cube (a low one when it closes). Automatic advances are silent. Nothing plays, and no audio context exists, unless this is on. |
| disabled | boolean | false | Dims the screen, stops the bars, and ignores taps, swipes and keys. |
| className | string | — | Merged onto the screen's classes; the screen is w-full at 9:16 unless an aspect class here says otherwise. |