Layers
Show real UI first as an exploded isometric stack of its layers, then settle the camera flat as the stack collapses into the real thing.
Use it for an entrance that shows how a piece of UI is put together: a hero demo, a product tour, a component on show. The demo starts as an exploded stack, the way a view-hierarchy debugger shows a screen: the frame at the bottom, its regions above it, its boxes above those and its content (text and icons) on top, each a thin plane with its depth label (z2 boxes 19). The planes come up one by one and draw their parts in build order. Then the camera settles from the isometric angle to flat while the gaps close layer by layer, and each plane hands over to the real parts it holds. Keep it to one moment on a page.
Installation
npm i @youj/motionFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Usage
Wrap the UI in LayersEntrance. It plays on its first child, on mount.
import { LayersEntrance } from '@youj/motion'
export function Hero() {
return (
<LayersEntrance>
<Dashboard />
</LayersEntrance>
)
}Change play to play it again (a counter works), or set it to false to show the UI as it is. onFinish is called when the run ends.
For an element you don't render with React, call the look. It returns a cancel() and a finished promise, and an AbortSignal in signal cancels it too. To drive it from a clock of your own (a video rendered frame by frame, a scrubber), pass paused: true and call seek(t) with the time in ms from its start: it shows that moment and holds it. It only goes forwards, and a time at or past its end ends it.
import { layers } from '@youj/motion'
const run = layers(document.querySelector('#dashboard')!, { speed: 1.5 })
await run.finishedPacing
Three stages: stack (the planes come up one by one, each drawing its parts as it arrives), collapse (the camera turns flat while the gaps close, bottom first) and resolve (the frame, then each plane in turn: its real parts fade in, then the plane fades off them).
The length grows with the demo: about 3 seconds for a card and about 5 for a small screen. duration fixes it in milliseconds, speed scales it, and phases shares it out as weights (default { stack: 45, collapse: 33, resolve: 22 }).
What it reads
The parts come from the real DOM, read once before the first frame (the same reading as Blueprint's), and are drawn on the planes in their real fills, borders, radii, type and shapes, so the flat stack is already close to the UI it hands over to. The camera is fitted so the exploded stack stays inside the demo's box and a small margin around it. A demo with no regions has three planes.
Reference
Propsplay, duration, speed, phases, onFinish
Props
| Prop | Type | |
|---|---|---|
childrenRequired | ReactNodeThe UI to play: its first element is the demo. | – |
play | boolean | number | stringPlays on mount and again whenever this changes; default true | true |
onFinish | () => voidCalled when a run ends, also early when focus moves into the UI (not when unmounting or a new play cancels it). | – |
speed | numberMultiplies the pace: 2 plays twice as fast. default 1 | 1 |
duration | numberThe whole run in ms, before | – |
phases | { stack?: number; collapse?: number; resolve?: number }Each stage's share of the run: the stack built up, its collapse to flat, the hand-over to the real parts. default `{ stack: 45, collapse: 33, resolve: 22 }` | `{ stack: 45, collapse: 33, resolve: 22 }` |
layers
function layers(node: HTMLElement, options?: LayersOptions): { cancel(): void; finished: Promise<void>; seek(t: number): void }| Option | Type | Default | Description |
|---|---|---|---|
duration | number | from the demo's size | The whole run in ms, before speed. |
speed | number | 1 | Multiplies the pace. |
phases | { stack?: number; collapse?: number; resolve?: number } | { stack: 45, collapse: 33, resolve: 22 } | Each stage's share of the run, as weights. |
labels | Partial<LayersLabels> | Youj's labels | The layers' names on the depth labels. <LayersEntrance> takes them from <YoujProvider labels>. |
signal | AbortSignal | Aborting cancels the run. | |
reduced | boolean | the provider, else the OS | Skip the entrance. |
personality | MotionPersonality | the theme's | The motion personality its timing and curves come from. |
paused | boolean | false | Hold the run at its start until seek(t) moves it, for a clock of your own. |
MotionStack, collapse, resolve · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Plane uptransform (translateZ) and opacity, one plane after another | duration.slowease.enter340ms · cubic-bezier(0.16, 1, 0.3, 1) | 340mscubic-bezier(0.16, 1, 0.3, 1) | |
| Parts drawneach part wipes onto its plane in build order | ease.standardcubic-bezier(0.2, 0, 0, 1) · the stack, 45% | cubic-bezier(0.2, 0, 0, 1)the stack, 45% | |
| Collapsethe camera turns flat; the gaps close one at a time, bottom first | ease.standardcubic-bezier(0.2, 0, 0, 1) · 33% | cubic-bezier(0.2, 0, 0, 1)33% | |
| Resolveper plane: its real parts fade in, then the plane fades off them | ease.entercubic-bezier(0.16, 1, 0.3, 1) · 22% | cubic-bezier(0.16, 1, 0.3, 1)22% |
Reduced motion
No stack: the UI is just there, and nothing is added to the page.
The stack is one preserve-3d stage on the overlay, moved by transform and opacity only. The demo is never restyled: its parts are held by their inline opacity while they wait and each arrives as a Web Animation. When the run ends, or cancel() is called, everything is put back exactly. Focus or a press in the demo ends the run at once.
Reduced motion
The UI shows as it is, with no overlay and no animation. layers() returns a run that has already finished.
AccessibilityARIA, focus, pointer
Accessibility
- The stack is
aria-hiddenand never takes the pointer. The demo stays in the accessibility tree the whole time. - Tabbing into the demo, or pressing on it, ends the run at once.
- The camera move is slow and eased; under reduced motion there is none.