Load Transition

Animated

The handover from skeleton to content, without the height jump.

View Source

#Installation

npx @dinachi/cli@latest add load-transition

#Usage

tsx
import { LoadTransition } from "@/components/ui/load-transition"
tsx
<LoadTransition loading={isLoading} skeleton={<Placeholder />}>
  <DeploymentSummary data={data} />
</LoadTransition>

#Examples

#The skeleton-to-content jump

Almost nobody treats this as an animation. The skeleton is removed, the real content is inserted at a different height, and everything below it jumps. The skeleton was supposed to be preventing exactly that.

Three things fix it, and only two of them are motion.

FixWhy it works
The container animates its own height across the swapThe page below settles instead of snapping. It is motion's layout animation, so the height change is a transform with a scale correction on the children rather than a real height tween, which keeps it off the main thread.
The skeleton leaves faster than the content arrives: 120ms out against 260ms inThere is never a frame with both at full strength, and never one with neither. A skeleton lingering over the real text is worse than a hard swap.
A skeleton that appears and disappears inside 150ms never appears at allA flash is worse than the wait it was hiding. That one is timing, not animation.

#Two thresholds, solving opposite problems

PropDefaultWhat it prevents
delay180Nothing shows for the first 180ms. A request that returns in 120ms goes straight to content; a placeholder shown for two frames reads as a glitch, not as feedback.
minimum420Once shown, the skeleton stays. A placeholder that appears and vanishes immediately is the same flash arriving from the other direction.

They are not motion, so reduced motion does not remove them.

useSkeletonVisibility(loading, delay, minimum) is exported on its own if you want the same two thresholds somewhere this component does not fit.

#The radius is an inline style

radius is a number in px, applied inline, not a class.

Motion can only counteract the distortion its own scale introduces on a value it is animating, and a class is invisible to it. As a class the corners visibly stretch during the resize.

#When to use it

Use it forNot for
A region whose loaded height differs from its placeholder: a summary, a card, a detail panel.A full-page load. There is nothing below it to protect from the jump.
Anywhere a skeleton already exists and is causing a reflow when it leaves.Content that streams in piece by piece. That is a different problem, and this component swaps once.

#Behaviour

  • The swap is mode="popLayout", so the leaving skeleton is pulled out of flow immediately and the arriving content does not wait behind it.
  • Both children carry layout, so the parent's scale is undone on them rather than squashing the rows inside.
  • Reduced motion drops the travel and the resize, and keeps the crossfade. The handover is still a handover; it just does not move.

#API Reference

PropTypeDefaultDescription
loadingbooleanWhether the data is still on its way. Not the same as whether the skeleton is on screen
skeletonReactNodeThe placeholder shown while waiting
delaynumber180Milliseconds of loading before the skeleton appears at all. A faster load goes straight to content
minimumnumber420Milliseconds the skeleton is held once shown, so it cannot flash
radiusnumber12Corner radius in px. A number, not a class — motion can only correct a value it is animating