Segue
One scene at a time. When you change it, one hairline crosses the frame carrying the next scene's number and name, and the next scene is already behind it.
Report an issue with SegueExample
Silence first.
The room before anyone speaks: paper, and the space the words will take.
Silence first.
The room before anyone speaks: paper, and the space the words will take.
Wipe1 of 3: Overture
Silence first.
The room before anyone speaks: paper, and the space the words will take.
Horizon1 of 3: Overture
Silence first.
The room before anyone speaks: paper, and the space the words will take.
Scrub
Silence first.
Then one line.
Then nothing again.
Scrub, horizon
Silence first.
Then one line.
Then nothing again.
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/segue.jsonUsage
This example also uses Button.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/ button.json"use client"import * as React from "react"import { Button } from "@/components/ui/button"import { Segue, SegueScene } from "@/components/ui/segue"const acts = [["Overture", "Silence first.", "The room before anyone speaks: paper, and the space the words will take."],["Theme", "Then one line.", "A single statement, set large, with nothing beside it to lean on."],["Coda", "Then nothing again.", "The last word stays a moment, and the page is quiet once more."],]function Scenes({ variant, size = "ot-ff" }: { variant?: "wipe" | "horizon"; size?: string }) {const [at, setAt] = React.useState(0)return (<div className="grid gap-8"><Segue value={at} onValueChange={setAt} variant={variant} aria-label="Three scenes">{acts.map(([label, line, note]) => (<SegueScene key={label} label={label} className="grid content-start gap-4 py-10"><p className={size} style={{ margin: 0, color: "var(--ot-ink)" }}>{line}</p><p className="ot-p" style={{ margin: 0, maxWidth: "34ch", color: "var(--ot-graphite)" }}>{note}</p></SegueScene>))}</Segue><div className="flex gap-6"><Button variant="quiet" disabled={at === 0} onClick={() => setAt(at - 1)}>Back</Button><Button variant="quiet" disabled={at === acts.length - 1} onClick={() => setAt(at + 1)}>Next scene</Button></div></div>)}export default function Example() {return (<div className="grid gap-y-24"><Scenes /><Scenes variant="horizon" /></div>)}Contract
- Underneath: native, plus a hook. With
scrub, it reads the scroll. - Anatomy:
<div class="ot-segue" data-variant="horizon" data-scrub>holding one.ot-segue-sceneper scene (data-currenton the one showing,data-labelits name; during a crossingdata-part="from | to"), each around aSegueScene(.ot-segue-body, a named group when it has alabel); then an aria-hidden.ot-segue-linewith its.ot-segue-caption(the number, then the name), and a polite.ot-srthat says "2 of 3: Theme" when the scene changes. While crossing the root carriesdata-phase="cross | scrub",data-edge(left | right | bottom | top, the edge the line comes in from) and--ot-segue-p(0 to 1, registered, so the pressed crossing is one transition). - One scene at a time, as a film cuts from shot to shot with a wipe. The scenes stand in one cell, so the frame is as tall as its tallest scene and nothing below it moves. Change
valueand one hairline in ink crosses the frame (--ot-andante,--ot-breath), carrying the next scene's number and name in small type on its trailing side, as the SHAPES / GRADIENTS rule carries a date and a name. Behind the line the next scene is already there, drifting its last--ot-segue-drift(27px) into place; ahead of it the last scene is pushed on by as much. Going back, the line crosses the other way. A second change mid-crossing arrives at once and crosses again from there. At rest there is no line, no frame and no chrome: only the scene. - It is the one hairline scene change for every dissolve (wave-wipe, dither-dissolve, grain-dissolve, pixel-swap): the change is the line, and it moves only when the person does.
- horizon (
data-variant="horizon"): after Eclipse, the soft side of one level line. The line rises from the foot and the next scene comes up under it, its name just under the line at the start; going back, it sets from the head. - scrub (
scrub,data-scrub): the crossings follow the scroll, after the scrub gather. As the frame's middle rises from four fifths of the way down the view to one fifth (or as far as the page can scroll), it passes through every scene in turn; the line stands as far across as the scroll has gone, stops when the scroll stops and goes back when you scroll back. The scene past halfway is the current one. It is read from the real layout in a frame asked for by the scroll event, which runs after a smooth scroller's own frame, so the line and the page move together. Every scene stays with readers, in order (the hidden ones are transparent, not removed), and there is no announcement. - Keyboard: the scenes hold their own controls; the ones not showing are
inertand hidden from readers outside a scrub. In a scrub all text remains readable in order, but inactive scenes accept no pointer activation and their controls leave the tab order. If focus is in a scene as it becomes inactive, it moves to the current scene's wrapper without scrolling. The control that changes the scene is yours: a quiet button, a key, an answer. - Right to left, the wipe comes in from the right. Reduced motion: the scene changes at once and nothing travels; a scrub changes halfway. Forced colours: the line is CanvasText and the caption drops its halo.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
SegueScene | div | A scene. Direct children of Segue only. `label` names it: the hairline carries the name as it crosses, and a labelled scene is a named group for readers. | |
value / defaultValue | number | 0 | Which scene is showing, from 0. Change it from your own control (a button, a key, an answer) and the line crosses; to an earlier scene it crosses the other way. A second change mid-crossing arrives at once and crosses again from there. |
onValueChange | (value: number) => void | Called with the new scene. With `scrub`, called as the scroll brings each scene past halfway. | |
variant | "wipe" | "horizon" | "wipe" | wipe: an upright hairline crosses along the line, from the start (the end, going back; right to left, it mirrors). horizon: after Eclipse, a level hairline rises from the foot and the next scene comes up under it; going back, it sets. |
scrub | boolean | false | Tie the crossings to the scroll. As the frame's middle rises from four fifths of the way down the view to one fifth, it passes through every scene in turn; the line stands as far across as the scroll has gone, stops when the scroll stops and goes back when you scroll back. Read from the real layout, so it keeps step with smooth scrolling. Every scene stays with readers, in order. |
SegueScene label | string | The name carried by the crossing hairline and the accessible name of the scene's group. Omit it for an unnamed scene. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Segue | The film editor's wipe; SHAPES / GRADIENTS (a hairline carrying a date and a name) | One hairline crosses and the next scene is behind it, its name carried on the line |
| Horizon segue | Eclipse | The line rises like a horizon and the next scene comes up under it |
| Scrub segue | The reader's own pace (the scrub gather) | The line stands as far across as the scroll has gone |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Segue | Wipe | The hairline crosses carrying the next scene's number and name; the next scene drifts in behind it and the last is pushed on; back, it crosses the other way |
| Horizon segue | Sunrise | The line rises from the foot and the next scene comes up under it; back, it sets |
| Scrub segue | Rubato (the reader's tempo) | The line travels only as far as the scroll has gone, forwards or back |