Skip to content

Conversation

@ZakaryH
Copy link
Contributor

@ZakaryH ZakaryH commented Feb 6, 2026

Motivations

currently AnimatedPresence (and AnimatedSwitcher) have a static timing, and don't allow you to choose the mode that is used for enter/exit.

the "popLayout" mode can cause issues with some layouts where the animated content is the last thing in the document, causing it to visually shift heights as the exiting element is immediately removed and for a fraction of a second, the other content isn't yet there. so, by allowing choice of exit/enter behavior, this can be tailored to the situation.

as for the timing, we have timing tokens. it seems odd that we would not allow other valid, consistent timing values to be used.

the callback is there so that a consumer can deal with some possible layout quirks such as the one we encountered. it may not solve this case, but I think it's a useful prop to have regardless.

Changes

Added

  • timing prop
  • exitBehavior prop
  • onExitComplete prop

I've intentionally tried to name the exitBehavior prop values with more "what"-style names. I do not want us to be tied to framer-motions API, and struggle to figure out what an equivalent is later down the road if we should swap the underlying library. using terms like "sequential" makes it clear to both a consumer and us what is desired VS "wait" which is the library's terminology.

Changed

Deprecated

Removed

Fixed

Security

Testing

try the various timing tokens, seee that they do what you expect.

try the new callback, see that it fires when you expect

for the exitBehavior I've added a webstory to demonstrate the difference between the exitBehavior prop value. to see the exact issue, what you'll need is pretty specific:

  • "fromBottom" animation
  • a layout that is tall enough that you have to scroll to get to the animated section
  • an AnimatedPresence around some elements that swap. think of tab content that changes out based on selections.
  • ensure that the container of the animated content only had padding, no actual height of its own. it just gets a height based on the content.

once we have that, the scenario is that you scroll down and start initiating the animations that swap. using "replace" is the current issue, so you can try that. "sequential" from some testing had the same issue though.

the reason being that the content gets shorter for a frame or two, and we're scrolled to a place that doesn't exist so the scroll/position moves for a moment.

aside from that, just try out the different exitBehavior prop values to make sure they look and feel good! keep in mind they can be combined with all the various transition values!

Changes can be
tested via Pre-release


In Atlantis we use Github's built in pull request reviews.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 6, 2026

Deploying atlantis with  Cloudflare Pages  Cloudflare Pages

Latest commit: a092313
Status: ✅  Deploy successful!
Preview URL: https://78e85784.atlantis.pages.dev
Branch Preview URL: https://job-150838-animatedpresence.atlantis.pages.dev

View logs

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

Published Pre-release for a9a05ad with versions:

  - @jobber/components@6.110.1-JOB-150838-a9a05ad.0+a9a05ad71

To install the new version(s) for Web run:

npm install @jobber/components@6.110.1-JOB-150838-a9a05ad.0+a9a05ad71

/**
* The timing of the animation.
*/
readonly timing?: Timing;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not 100% on this interface

you'll be passing values like "timing-slow" rather than just "slow"

buut it is derived from the tokens which is nice. the only time it might become a hindrance is if we change the token values, the props would need to get fixed in the invocations.


export type AnimatedPresenceTransitions = keyof typeof transitions;

type ExitBehavior = "overlap" | "replace" | "sequential";
Copy link
Contributor Author

@ZakaryH ZakaryH Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me know if these are good, clear names.

I had a couple other ideas to describe "overlap" (crossfade)
and "immediate" instead of "replace"

the actual framer motion values they map to are:

  • "overlap" -> "sync"
  • "replace" -> "popLayout"
  • "sequential" -> "wait"

and here's their descriptions

    /**
     * Determines how to handle entering and exiting elements.
     *
     * - `"sync"`: Default. Elements animate in and out as soon as they're added/removed.
     * - `"popLayout"`: Exiting elements are "popped" from the page layout, allowing sibling
     *      elements to immediately occupy their new layouts.
     * - `"wait"`: Only renders one component at a time. Wait for the exiting component to animate out
     *      before animating the next component in.
     *
     * @public
     */
    mode?: "sync" | "popLayout" | "wait";

@ZakaryH
Copy link
Contributor Author

ZakaryH commented Feb 9, 2026

while I could see these changes being useful in general, they don't actually solve the problem that spawned this request.

ultimately it's a layout issue that happens to have an animation on it.

trying to fit AnimatedPresence into the equation only makes things harder because it truly does conditionally render in order for the animation to apply, however that makes determining/setting the minHeight really tricky where you have to start using useLayoutEffect refs and more to figure out what your min height should be on the container to avoid shifts.

image

OR you can just let CSS grid layouts figure out all that for you, and implement a small CSS animation yourself which is what I ended up doing in the problematic scenario.

I don't really want AnimatedPresence to have to worry about those layout concerns, nor is it well equipped to handle them in the first place.

@ZakaryH
Copy link
Contributor Author

ZakaryH commented Feb 9, 2026

see above

@ZakaryH ZakaryH closed this Feb 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

1 participant