# Entering and leaving

> Make things appear and disappear well, from the right size, out of the right place, with a quieter exit than entrance.

Module: Motion · Level: beginner · 9 min · https://school.agents.org.in/design/motion/enter-and-exit

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

*[Interactive demo: 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 opacity (How see-through something is, from 0 (invisible) to 1 (solid). Fading opacity is cheap to animate and does most of the work of making things appear) does the work of making it visible.

```css
.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.

> **In plain words.** **transform** moves, scales or rotates an element without disturbing anything around it. **opacity** is how see-through it is, from 0 (invisible) to 1 (solid). Together they do almost all good UI motion.

## 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.

```css
.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 transform-origin (The pivot point for scaling or rotating an element. Setting it to where a menu's button is makes the menu look like it grows out of that button), which is the center by default. Press the ••• button in each mode.

*[Interactive demo: Center origin grows from nowhere. Origin on the button grows from your press.]*

```css
.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 transform (The CSS property that moves, scales and rotates an element without disturbing anything around it. It's one of the two properties that are cheap to animate):

- **`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.

*[Interactive demo: 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 toast (A small message that slides in at the edge of the screen to confirm something ("Changes saved") and leaves on its own. One with an Undo button has to stay longer and pause while you look at it) 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.

*[Interactive demo: 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.

*[Interactive demo: 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.

```css
@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 (paste into your agent):**

```text
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.
```

**Check yourself:** A tooltip enters with scale(0.94), opacity and a small translate. What should its exit do?

- [ ] Play the same three properties in reverse at the same speed
- [x] Fade out with opacity (maybe a hint of scale), faster than it entered
- [ ] Scale down to 0 so it disappears into its anchor
- [ ] Nothing; remove it instantly with display: none

*Why:* Compose on enter, subtract on exit: the leaving element keeps the essentials and drops the theatre, at about 60–80% of the entrance time. Instant removal looks broken; scale(0) is the balloon.

## In the skill

- https://school.agents.org.in/skill/motion/never-scale-from-zero.md
- https://school.agents.org.in/skill/motion/transform-mastery.md
- https://school.agents.org.in/skill/motion/compose-subtract-asymmetry.md
- https://school.agents.org.in/skill/motion/sonner-principles.md
- https://school.agents.org.in/skill/motion/multi-segment-shake.md
