Skip to content
Twenty One Media
webSeptember 3, 2026

Five Animations That Never Fired

During a homepage pass last week, we ran a viewport check at 1440x900 with no scroll: load the page, freeze it, and look at what's actually on screen versus what the browser reports.

One of the pain cards below the fold was sitting at opacity: 1. It should have started at opacity: 0 and animated in when it entered the viewport.

We dug in. The reveal wrapper was there. The stagger prop was set. The component looked correct. So we checked the others.

Five of the six section wrappers on the homepage had never animated, not once, not for any visitor since the site launched.

What the Code Said vs. What Framer Did

The pattern we'd been using:

<AnimatedReveal stagger={0.1}>
  <li>Pain point one</li>
  <li>Pain point two</li>
  <li>Pain point three</li>
</AnimatedReveal>

Inside AnimatedReveal, setting stagger wires up staggerChildren on the parent variant. The assumption: Framer Motion will propagate that stagger to the children automatically.

It won't.

staggerChildren controls the timing between children that themselves declare variants. A plain <li> or <div> with no variants prop is invisible to the stagger system. Framer sees the parent orchestrating children, finds no variant keys to animate on those children, and does nothing.

The parent animates. The children sit at opacity: 1 and never move.

The Fix

We added a RevealItem component, a thin motion.div that declares the same variant keys the parent is orchestrating:

// Inside AnimatedReveal.tsx
export function RevealItem({ children }: { children: React.ReactNode }) {
  return (
    <motion.div variants={itemVariants}>
      {children}
    </motion.div>
  );
}

Then in the sections that needed stagger:

<AnimatedReveal stagger={0.1}>
  <RevealItem><li>Pain point one</li></RevealItem>
  <RevealItem><li>Pain point two</li></RevealItem>
  <RevealItem><li>Pain point three</li></RevealItem>
</AnimatedReveal>

Now Framer has variant keys to target, the stagger fires, and each item rises in sequence as the section scrolls into view.

Why This Slipped Through

The visual result of a broken stagger looks fine at first glance. The elements are visible. They don't flash or break layout. The parent container animates in, so there's a fade on the section wrapper. You'd have to know the children were supposed to stagger to notice they weren't.

We caught it because we were looking for it specifically: checking opacity values in DevTools on elements that should have started hidden. Most QA passes don't do that.

What We Audit Now

For any page with entrance animations, we now check two things before shipping:

  1. Open DevTools, pause before scroll, and inspect opacity on each animated element. If it's 1 at mount, the animation didn't register.
  2. For any Framer stagger pattern, every child that should stagger needs a variants prop or a motion wrapper that carries one. Plain HTML elements won't receive the orchestration.

The animations on the homepage now work: hero block, pain cards, capabilities, process, offers, portals. All six. The stagger timing on each one was already right. We just had to give Framer something to animate.

The code looked correct. The intent was correct. The assumption about how Framer propagates variants was wrong, and it was wrong quietly for months.

That's the kind of bug a good viewport audit catches.