# Framer Motion Sticky Scroll Layout

> Fix Framer Motion sticky scroll sections where absolute-positioned children don't render correctly. Use when: (1) console warning "Please ensure that the container has a non-static position", (2) sticky scroll pillar/slide transitions show content at wrong positions or invisible, (3) absolute-positioned motion.div children overflow or stack incorrectly inside a sticky viewport. Solution: wrap with relative container inside the sticky element.

- Skill: `hubeiqiao/framer-motion-sticky-scroll-layout` (Agent Skill)
- Install (CLI): `npx skillmds@latest add hubeiqiao/framer-motion-sticky-scroll-layout`
- Raw SKILL.md: https://api.skillmd.com/api/skills/hubeiqiao/framer-motion-sticky-scroll-layout/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: hubeiqiao (https://skillmd.com/u/hubeiqiao)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/hubeiqiao/framer-motion-sticky-scroll-layout

---


# Framer Motion Sticky Scroll Section Layout

## Problem
When building scroll-driven sections with CSS `position: sticky` and Framer Motion animated
slides (absolute-positioned children that fade in/out based on scroll progress), the children
render incorrectly — often stacking, overflowing, or appearing at wrong positions. Framer
Motion logs: "Please ensure that the container has a non-static position."

## Context / Trigger Conditions
- Building a scroll-pinned section with multiple "slides" that transition as user scrolls
- Using `position: sticky` with a tall scroll runway (e.g., `height: 300vh`)
- Child slides use `position: absolute; inset: 0` to stack on top of each other
- Framer Motion `animate` toggles `opacity` and `y` based on `useScroll` + `useTransform`
- Console warning about non-static position on the container

## Solution

**Correct structure:**
```tsx
<div ref={scrollRef} className="relative" style={{ height: '300vh' }}>
  {/* Sticky viewport */}
  <div className="sticky top-0 h-screen overflow-hidden">
    {/* CRITICAL: This relative wrapper is the positioning context */}
    <div className="relative w-full h-full">

      {/* Slide 1 */}
      <motion.div
        className="absolute inset-0 flex items-center"
        animate={{ opacity: currentSlide === 0 ? 1 : 0 }}
      >
        <div className="max-w-6xl mx-auto w-full px-8">
          {/* Content */}
        </div>
      </motion.div>

      {/* Slide 2 */}
      <motion.div
        className="absolute inset-0 flex items-center justify-center"
        animate={{ opacity: currentSlide === 1 ? 1 : 0 }}
      >
        {/* Content */}
      </motion.div>

    </div>
  </div>
</div>
```

**Key rules:**
1. The `relative w-full h-full` div MUST be a direct child of the sticky container
2. All slides use `absolute inset-0` to fill this relative container
3. Content padding (`px-8`, `max-w-6xl`) goes INSIDE each slide, not on the container
4. Use `flex items-center` on each absolute slide for vertical centering
5. Each slide gets its own layout (grid, flex, centered) — they don't share a parent layout

**Common mistake — putting content wrapper between sticky and slides:**
```tsx
// WRONG: max-w-6xl wrapper between sticky and absolute slides
<div className="sticky top-0 h-screen">
  <div className="max-w-6xl mx-auto w-full px-8">
    <motion.div style={{ position: 'absolute', inset: 0 }}>
      {/* This breaks because absolute child escapes the static parent */}
    </motion.div>
  </div>
</div>
```

## Verification
- Console warning about non-static position disappears
- All slides render centered vertically in the viewport
- Slides transition smoothly as scroll progresses through the runway
- Content has proper padding from viewport edges on all slides

## Notes
- The `overflow-hidden` on the sticky div prevents scroll-driven content from bleeding
- For `useScroll` with `target`, the ref goes on the outer runway div, not the sticky div
- Map scroll progress to slide index: `useTransform(scrollYProgress, [0, 0.33, 0.34, 0.66, 0.67, 1], [0, 0, 1, 1, 2, 2])`
- On mobile, skip sticky entirely — use a simple stacked layout with `whileInView` animations instead
- Use `useState` + `useEffect` with `motionValue.on('change')` to convert Framer Motion values to React state for conditional rendering

