Entering and leaving
beginner·9 min·Markdown for agents
What this lesson teaches
Make things appear and disappear well, from the right size, out of the right place, with a quieter exit than entrance.
Open both dialogs. The left one inflates from a dot like a balloon. The right one simply arrives.
The left one starts at scale(0). For its first few frames it's invisible, then a blurry speck, then a toy growing out of a single point. The right one starts at scale(0.95) with opacity: 0. Nothing in a UI should scale up from zero. The 5% size change is enough to read as motion; the does the work of making it visible.
.dialog {
opacity: 0;
transform: scale(0.95);
transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out);
}
.dialog[data-state="open"] {
opacity: 1;
transform: scale(1);
}Both properties share one duration and one curve, so they read as one move.
Appearing the first time
To animate something the moment it's added to the page, modern CSS has @starting-style: you describe where it starts, and the browser animates from there.
.toast {
opacity: 1;
transform: none;
transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out);
}
@starting-style {
.toast { opacity: 0; transform: translateY(8px); }
}Where that isn't supported, the common React pattern is a data-mounted attribute set one frame after mount, with the start state in the base rule and the end state under [data-mounted="true"]. Either way, list the properties; never transition: all.
Grow from where it came from
A menu that opens from a button should look like it unfolds out of that button. The pivot point for a scale is the , which is the center by default. Press the ••• button in each mode.
Press the ••• button in each mode. The scale is exaggerated here so you can see where it grows from.
.popover { transform-origin: var(--popover-origin, top right); }Radix, React Aria and Floating UI all tell you where the trigger is, so set the origin from that. The exception is a modal: it isn't attached to anything, so it stays centered.
Three transform tricks worth knowing
These come from Emil Kowalski's notes on :
translateY(100%)moves an element by its own height. A sheet hidden with it works whether it's 200px or 800px tall; no magic numbers.scale()scales the children too, text included. If a card grows on hover, its text grows with it. Scale an icon or image inside instead.- Order matters, and
transformis one property.translate(10px) rotate(45deg)is notrotate(45deg) translate(10px). And writingtransform: scale(1.05)on hover silently throws away atranslateYset elsewhere; write both.
Leave quieter than you arrived
Show and hide this check. The first exit plays the entrance backwards. The second keeps three quiet properties and lets go.
Show and hide it in both modes. The rewind is correct and still feels mechanical; the quiet exit just lets go.
This is a signature pattern from Jakub Antalik's transitions.dev: compose on enter, subtract on exit. The entrance is the news, so it can use several properties (opacity, scale, a little rotation, a bob, a stroke drawing on). The exit is "going away", so it keeps the essentials (opacity, a hint of scale, a 2px blur) and drops the theatre. A rewound entrance is technically correct and still feels mechanical. Pair it with the shorter exit timing from the last lesson.
Don't skip the exit entirely, though. Something that pops onto the screen with care and then vanishes in one frame looks broken.
Toasts: a small library's worth of lessons
A is the little message that slides up to confirm something ("Invite resent"). Emil Kowalski's library Sonner is the most copied toast on the web, and its principles apply to any component you hand to other people.
- The API is the product.
toast("Saved")beatstoast({ message, type, duration }). Default everything. - Good defaults beat options. "What happens if I pass nothing?" should always look right.
- Handle edge cases quietly: stack many toasts, dedupe identical ones, survive rapid clicks.
- Transitions, not keyframes, so spam-dismissing reverses smoothly instead of restarting.
- Match motion to personality. Sonner is calm, so its toasts use a slower ~400ms
ease. Vaul is physical; cmdk is snappy. Pick one and stay consistent. - Leave by the way you came: in from the bottom, out through the bottom, with fewer properties.
- Look again tomorrow. Build it today, review it after sleep. The cheapest QA there is.
Errors: a shake that means it
One more entrance: the error. When a form field fails on submit, a short shake points at it. Try both styles with an empty field.
The version that reads as physical, from Jakub Antalik: 280ms, five stops, shrinking from 4px to 2px, on a snappy curve.
@keyframes shake {
0% { transform: translateX(0); }
28.57% { transform: translateX(-4px); }
57.14% { transform: translateX(4px); }
78.57% { transform: translateX(-2px); }
100% { transform: translateX(0); }
}
.field--error { animation: shake 280ms cubic-bezier(0.36, 0.07, 0.19, 0.97); }Shake the field's wrapper, never the input itself, or the text cursor jumps. Keep the red border after the shake (the shake gets attention; the state explains). Fire it on submit or blur, never on each keystroke.
Review how elements enter and leave in this UI. Replace any scale(0) with scale(0.95) plus opacity 0 → 1 on the same duration and ease-out curve. Set transform-origin on popovers and menus to the trigger's position (modals stay centered). Make exits shorter (about 70% of the entrance) and quieter: drop translate, rotate and bounce on exit, keep opacity and a small scale. For form errors, shake the field wrapper 280ms with keyframes at 0, 28.57, 57.14, 78.57, 100% (0, -4px, 4px, -2px, 0) and keep the error border afterwards.
Quick check
A tooltip enters with scale(0.94), opacity and a small translate. What should its exit do?
In the skill
The nodes your agent reads when it does this work.
- Never animate from scale(0)Never animate from scale(0). Use scale(0.95) + opacity, or @starting-style for modern CSS entry.
motion/never-scale-from-zero - CSS transform masterytranslateY with percentages, scale() scales children, 3D transforms, transform-origin fundamentals.
motion/transform-mastery - Compose on enter, subtract on exitEnter with 5 properties, exit with 3. Disappearance feels soft, not a reversed entrance.
motion/compose-subtract-asymmetry - Sonner principlesTen principles for building loved components, distilled from Emil Kowalski's Sonner library.
motion/sonner-principles - Multi-segment shakeForm-error shake at 0%, 28.57%, 57.14%, 78.57%, 100% over 280ms with 4px overshoot.
motion/multi-segment-shake
Connected lessons
What this lesson builds on and leads to, across every course.