Recipes provide state selectors, transition and animation behaviour, timing,
easing, transform origin where the behaviour requires it, and reduced-motion
handling.Recipes do not provide colors, typography, dimensions, spacing, shadows,
component structure beyond the minimum the movement requires, framework
components, or application state.
tm-presence-*, and an element with no recognised state is treated as open.
Menu and popover
data-side. A menu
should look like it comes out of its trigger, not out of its own middle.
The names follow the convention Radix UI, Base UI and Floating UI already emit.
Alignment is deliberately not mapped.
transform-origin takes no logical
keywords, so a data-align rule would hard-code left or right and break in
right-to-left contexts. Positioning libraries already publish a resolved origin —
pass it straight through with style="--tm-origin: var(--radix-popper-transform-origin)".[popover], use tm-native-popover
instead: the browser handles the top layer and light dismiss.
Dialog
<dialog> element use tm-native-dialog,
which gets the backdrop and the top layer for free.
Toast
role="status" is yours to add: motion is not an announcement.
Tooltip
data-side sets the transform origin and the retreat direction — a tooltip above
its trigger retreats upward, one below it downward.
Accordion
0fr → 1fr grid row is the one height animation CSS can express without
measuring anything, so this needs no JavaScript and no fixed height.
Support is Chrome 107, Safari 16, Firefox 66. Older browsers show and hide the
panel without the height tween — open content is never hidden.
For a <details> element use
tm-native-disclosure.
Tabs
Two independent pieces, because they have different requirements. The panel cross-fade needs no measurement:translate and inline-size between whatever two values you
give it. It never reads the DOM — see
the zero-runtime boundary.
Loading to success
data-state="idle" | "loading" | "success" | "error"; a button with no data-state shows the idle label.
Success is the only state in the library that arrives with a spring, because it
is the one moment in a form worth celebrating. --tm-overshoot: 0, or a calm
profile, removes it.
Hold to confirm
- Linear easing is correct here and almost nowhere else. The fill is a clock, and any other curve would misreport how much time is left.
- The fill inherits the button’s own color through
--tm-colorat 18% opacity. TailMotion picks no red.overflowandisolationare set because the fill has to be clipped to the button’s box; your radius, padding and color stay yours. - It keeps its full duration under reduced motion, because it is a readout of elapsed time rather than decoration. Collapsing it to 1ms would confirm a destructive action the instant the button was touched.
The older
tm-hold-delete still works and is unchanged. It chooses its own
colors, padding and radius, which is exactly why tm-hold-confirm exists.Recipe timing at a glance
All of these scale with the motion profile and
accept the usual
tm-duration-* and --tm-exit-duration overrides.