Skip to content

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/motion

import { LayersEntrance } from '@youj/motion'

For 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.finished

Pacing

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

LayersEntrance props
PropType
childrenRequiredReactNode

The UI to play: its first element is the demo.

–
playboolean | number | string

Plays on mount and again whenever this changes; false shows the UI as it is.

default true

true
onFinish() => void

Called when a run ends, also early when focus moves into the UI (not when unmounting or a new play cancels it).

–
speednumber

Multiplies the pace: 2 plays twice as fast.

default 1

1
durationnumber

The whole run in ms, before speed. Default: from the demo's size and part count.

–
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 }
OptionTypeDefaultDescription
durationnumberfrom the demo's sizeThe whole run in ms, before speed.
speednumber1Multiplies the pace.
phases{ stack?: number; collapse?: number; resolve?: number }{ stack: 45, collapse: 33, resolve: 22 }Each stage's share of the run, as weights.
labelsPartial<LayersLabels>Youj's labelsThe layers' names on the depth labels. <LayersEntrance> takes them from <YoujProvider labels>.
signalAbortSignalAborting cancels the run.
reducedbooleanthe provider, else the OSSkip the entrance.
personalityMotionPersonalitythe theme'sThe motion personality its timing and curves come from.
pausedbooleanfalseHold the run at its start until seek(t) moves it, for a clock of your own.
MotionStack, collapse, resolve · reduced motion

Motion

Motion tokens
MomentTokens
Plane uptransform (translateZ) and opacity, one plane after another
duration.slowease.enter

340ms · 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.standard

cubic-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.standard

cubic-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.enter

cubic-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-hidden and 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.