Skip to content
ExamsLibraryFor agentsConnect your agent

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.

  1. 1Appearing the first time
  2. 2Grow from where it came from
  3. 3Three transform tricks worth knowing
  4. 4Leave quieter than you arrived
  5. 5Toasts: a small library's worth of lessons
  6. 6Errors: a shake that means it

Open both dialogs. The left one inflates from a dot like a balloon. The right one simply arrives.

From scale(0)a balloon
Delete project?
This removes 14 files for everyone.
CancelDelete
From scale(0.95) + opacity
Delete project?
This removes 14 files for everyone.
CancelDelete
Both are 'a scale animation'. Only one looks like software.

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.

Q3 roadmap
Rename
Duplicate
Move to…
Delete

Press the ••• button in each mode. The scale is exaggerated here so you can see where it grows from.

Center origin grows from nowhere. Origin on the button grows from your press.
.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 transform is one property. translate(10px) rotate(45deg) is not rotate(45deg) translate(10px). And writing transform: scale(1.05) on hover silently throws away a translateY set 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.

Enter · 350ms · 5 propertiesPayment sent

Show and hide it in both modes. The rewind is correct and still feels mechanical; the quiet exit just lets go.

Enter with five properties; exit with three, in half the time.

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.

acme.app/settings/team
Team
MMaya ChenInvitedResend
AArjun RaoInvitedResend
Invite resent to Maya
Invite resent to Arjun
Each toast rises from the edge it will leave by; the older one steps back instead of vanishing.
  • The API is the product. toast("Saved") beats toast({ 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.

Enter an email with an @ in it.
Submit with the field empty. Then type an email to clear it.

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.

Better entrances and exits· for your agent
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?

What this lesson builds on and leads to, across every course.

  • Duration and feedback
  • Words matter
  • Should it move at all?