Search code examples
framer-motion

Framer Motion: Shared layout animations stop working when motion.div grandparent changes flex-direction


I'm trying to replicate this effect where the active tab indicator is animated.

My CodeSandbox

Expected behavior: The navlink's highlight should be animated regardless of the flex container's direction.

Current behavior: It only animates when flex-direction is column, not row.

Line 25: Here's the orange highlight:

{hoverAnchor === link && (
  <motion.div
    layoutId="underline"
    transition={{ duration: 0.15 }}
    className="absolute bottom-0 left-0 h-[2px] w-full bg-orange"
  />
)}

Line 58: Here's the <motion.ul> grandparent, which is a flexbox: column when screen size <= 640px, otherwise row:

<motion.ul
  initial={{ x: 'var(--menu-offscreen)' }}
  animate={{ x: menuOpen ? 0 : 'var(--menu-offscreen)' }}
  onMouseLeave={() => setHoverAnchor('')}
  className="fixed left-4 top-4 flex h-[calc(100vh_-_2rem)] w-[calc(100vw_-_2rem)] flex-col rounded-lg bg-white py-4 [--menu-offscreen:-400px] xs:max-w-[368px] sm:static sm:h-auto sm:w-auto sm:max-w-full sm:flex-row sm:py-0 sm:outline-none sm:[--menu-offscreen:0px]"
>

It will animate correctly if flex-direction is column

It will animate correctly if I turn the grandparent <motion.ul> back into a regular <ul>, but I don't want to because it has its own animation to do.

Not sure what I'm missing, and the docs doesn't seem to say anything about nesting motion divs, or I couldn't find one.


Solution

  • I tinkered around and found the problem lies in the CSS variables, not <motion.ul> itself:

    <motion.ul
      initial={{ x: 'var(--menu-offscreen)' }}
      animate={{ x: menuOpen ? 0 : 'var(--menu-offscreen)' }}
      className="[--menu-offscreen:-400px] sm:[--menu-offscreen:0px]"
    >
    

    I was trying to animate x based on CSS variable --menu-offscreen which changes depending on Tailwind's breakpoints. The problem disappears if I don't use var(--menu-offscreen) in intial and animate, so I replace it with a fixed number, and now I handle the breakpoint changes with React instead of Tailwind. It looks something like this:

    {/* react state */ smallScreen && (
      <motion.ul
        initial={{ x: -400 }}
        animate={{ x: 0 }}
      />
    )}