Skip to content

Inspector

Show real UI first as the browser's box model, part by part, with a hover reading out each size, then hand each part to the real thing.

Use it for an entrance that shows how a piece of UI is laid out: a hero demo, a component on show, a layout walkthrough. Each part appears first the way a devtools overlay draws it: its margin, padding and content in three soft tints with a crisp edge. A hover runs through the tree ahead of them with a mono size tag (div 72 × 40, p 14/20) and the element's path under the demo. Then each part hands off to the real one, the frame first. Keep it to one moment on a page.

Installation

npm i @youj/motion

import { InspectorEntrance } 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 InspectorEntrance. It plays on its first child, on mount.

import { InspectorEntrance } from '@youj/motion'

export function Hero() {
  return (
    <InspectorEntrance>
      <PricingCard />
    </InspectorEntrance>
  )
}

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 { inspector } from '@youj/motion'

const run = inspector(document.querySelector('#pricing')!, { speed: 1.5 })
await run.finished

Pacing

Two stages: inspect (every box lays down its model in build order, wiping in from its reading corner, while the hover runs through them) and handoff (the frame, then each piece in build order, fades in under its model, and the model fades off it). The hover rests on a part a little longer than a base beat, so on a busy screen it reads a sample of the parts, evenly through the run.

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 { inspect: 60, handoff: 40 }).

What it reads

The parts come from the real DOM, read once before the first frame (the same reading as Blueprint's). Margins, borders and padding come from each element's computed style. The content is tinted in the theme's info ink, the padding in whichever of success and warning sits farther from it (so an amber accent never makes the two alike), and the margin is hatched in a neutral. A box that holds others is drawn by its edges and bands alone, so nested tints never pile up. The tag names an element by its tag and its data-slot (or id); text shows its font size and line height.

Reference

Propsplay, duration, speed, phases, onFinish

Props

InspectorEntrance 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{ inspect?: number; handoff?: number }

Each stage's share of the run: the box models drawn in with the hover running through them, then the hand-off to the real parts.

default `{ inspect: 60, handoff: 40 }`

`{ inspect: 60, handoff: 40 }`

inspector

function inspector(node: HTMLElement, options?: InspectorOptions): { 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{ inspect?: number; handoff?: number }{ inspect: 60, handoff: 40 }Each stage's share of the run, as weights.
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.
MotionInspect, handoff · reduced motion

Motion

Motion tokens
MomentTokens
Inspecteach box model wipes in from its reading corner, in build order
ease.standard

cubic-bezier(0.2, 0, 0, 1) · 60% of the run

cubic-bezier(0.2, 0, 0, 1)60% of the run
Hoverglides from box to box; the tag and path change halfway across
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)
Handoffthe frame, then each piece: the part fades in, then its model fades off it
no token

40%

40%
Part inopacity
ease.enter

cubic-bezier(0.16, 1, 0.3, 1)

cubic-bezier(0.16, 1, 0.3, 1)
Model outopacity on the piece’s own canvas
ease.standard

cubic-bezier(0.2, 0, 0, 1)

cubic-bezier(0.2, 0, 0, 1)

Reduced motion

No overlay: the UI is just there, and nothing is added to the page.

The demo is never restyled. Its parts are held by their inline opacity while they wait (no animation, so no compositor layer) and each arrives as a Web Animation made just before its moment. Each piece's model is drawn once on a canvas of its own, so its hand-off is a single opacity 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. inspector() returns a run that has already finished.

AccessibilityARIA, focus, pointer

Accessibility

  • The overlay 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.
  • Nothing flashes: the tints are soft and every change is a fade or a wipe.