Motion in practice
Writing enter and exit transitions against data-starting-style and data-ending-style, with a reduced-motion fallback that is per token, not a kill switch.
The short version
NOT IMPLEMENTED. This component does not exist in any released version of opsinjs. There is no package to install, no module to import and no props interface to generate code against. Everything on this page is a specification of intended behaviour and may change without notice. Do not write code against it.
This component is not built yet
PlannedRoadmapWhat “planned” means
You do not need an animation library. Base UI keeps a leaving element mounted and marks the lifecycle with attributes; you write ordinary CSS transitions against them.
.sheet {
transition: opacity var(--opsin-motion-enter), translate var(--opsin-motion-enter);
opacity: 1;
translate: 0 0;
}
.sheet[data-starting-style],
.sheet[data-ending-style] {
opacity: 0;
translate: 0 1rem;
}Three lines and both directions work. The from-state and the to-state are the same declaration block because "off screen" is the same place whether you are arriving or leaving.
How it works
data-starting-style is present for one frame as the element enters, which
gives the browser a state to transition from. data-ending-style is present
while the element is leaving; Base UI keeps it mounted until the transition
finishes, then unmounts it. Between the two, the element has neither attribute
and sits at its resting style.
The timing values are tokens, not literals. opsinjs motion tokens are linear()
easings generated from spring parameters, so a transition reads as one custom
property rather than as a duration and a curve you tuned by eye. The reasoning
is in Springs as tokens.
Reduced motion
prefers-reduced-motion: reduce is not a request to remove feedback. It is a
request to remove movement. A dialog that appears with no transition at all is
harder to follow than one that cross-fades, and a reader who has set the
preference still needs to know that something arrived.
opsinjs therefore degrades per token: each motion token has a reduced-motion counterpart that keeps a short opacity change and drops the translation, scale and any rotation. This is handled in the token layer, so a transition written against a token gets the correct fallback without your writing a media query. The rule and its rationale are in Reduced motion.
Below is a real transition on a real element, with a checkbox that simulates the
preference so the two can be compared without changing your system settings. The
token pair is the one Sheet uses.
--opsin-ease-spring-sheet · --opsin-duration-spring-sheet. Under prefers-reduced-motion: reduce this becomes 1ms with a flat curve. The element still moves, and it simply arrives at once. The checkbox simulates the preference; if you have actually set it, app/globals.css has already applied it and the two states will look the same.
Do this
- Transition properties that do not trigger layout:
opacity,transformortranslate/scale, andfilter. These composite on the GPU;width,height,topandmargindo not. - Use the same declaration block for starting and ending style unless the two directions genuinely differ. A sheet that slides up on entry and fades on exit is usually a mistake rather than a decision.
- Key off the token, not a number.
var(--opsin-motion-enter)carries the duration, the curve and the reduced-motion behaviour together. - Let the element leave. If you control mounting yourself, keep the element in the tree until the exit finishes, or use the component's own open state and let it handle it.
- Test with the preference on. It is a one-line change in system settings and it is the only way to find a transition that was carrying meaning.
Not this
- Never carry urgency in motion. A pulsing or shaking alert conveys nothing to a reader with the preference set, nothing in a screenshot, and nothing to anyone using assistive technology. At the same time, it is actively harmful to people with vestibular disorders. This is doctrine, not preference; see Motion in health UI.
- Do not animate a number into place. A value that counts up from zero is briefly displaying figures that are not the reader's. In a health context that is a false statement rendered at 60fps.
- Do not use a blanket
* { transition: none }under reduced motion. It removes the opacity fallback too, which makes state changes harder to follow. - Do not reach for a spring library. The tokens already are springs,
serialised to
linear(), and they run on the compositor rather than in JavaScript. - Do not transition
display. It does not interpolate. The fix is a discrete transition plus@starting-style, and it is not needed here because Base UI's mount lifecycle already solves it.
Gotchas
- You cannot see
data-starting-stylein devtools. It lasts one frame. An enter transition that "does not fire" is almost always a missing from-state rule rather than a missing attribute. - Transitioning a property the element has never had produces no transition.
If the resting style does not declare
translate, there is nothing to interpolate from. @starting-styleanddata-starting-styleare different things. The first is a CSS at-rule for entry animations on newly rendered elements; the second is Base UI's attribute. You need only the second, and mixing them produces two competing sources of the same intent.- Exit transitions do not run if you unmount the element yourself. No error is reported; the element simply vanishes.
- A transition on a composited surface costs more than you think. Animating a blurred material is one of the few things that will actually drop frames on a mid-range phone; see Performance budget.
- Reduced motion is not the same as reduced transparency. Two separate queries, two separate contracts; see Reduced transparency.
Related
- Springs as tokens is where the
linear()values come from. - Reduced motion has the per-token degradation contract.
- Data attributes has the full lifecycle vocabulary.
- Motion in health UI is the doctrine that constrains all of the above.
Forms
Wiring a Field to React Hook Form, TanStack Form or a native form. The validation boundary, server errors, and the accessibility the wiring must not lose.
Icons
Bring your own icon set. This page covers the slot contract, the sizing rule, and the hard prohibition on an icon carrying clinical status by itself.