When Scrollytelling Breaks Mobile: A CSS Transform Detective Story
We built a beautiful scrollytelling experience with parallax effects and dynamic headers. It was perfect on desktop. Then came the message: "The header isn't showing on my iPhone." This is the story of a three-day debugging journey into CSS stacking contexts.

Dr. Chad Okay
NHS Resident Doctor & Physician-Technologist
When Scrollytelling Breaks Mobile: A CSS Transform Detective Story
How I Discovered Why position:fixed Fails Inside Transformed Parents
"The header's completely invisible on mobile."
It was 9pm on a Friday. I'd just deployed the scrollytelling hero I'd been crafting for two weeks. My MacBook showed perfection. My iPhone showed... nothing. The header had vanished. Not broken, not misaligned. Gone.
Three days and seventeen Stack Overflow tabs later, I finally understood why CSS transforms had betrayed me.
The Dream That Started This Nightmare
Picture this: It's 2am on a Tuesday in early September. I'm building PhysicianTechnologist's homepage, fuelled by the third coffee that definitely should have been decaf. The vision was intoxicating: a scrollytelling experience that would make medical content as engaging as those award-winning parallax gaming sites I'd been obsessing over.
I wanted parallax effects that would make readers forget they were learning about clinical AI. Dynamic headers that knew precisely when to appear. Smooth animations guiding visitors through complex healthcare topics like a gentle hand on their shoulder.
On 14th of July at 4pm, I shipped it. On my MacBook, it was magnificent.
At 6:47pm, my phone buzzed. A message from a friend: "The header isn't showing on my iPhone."
My stomach dropped. That particular feeling when you realise you've tested everything except the thing that actually matters.
Act 1: The Perfect Desktop Illusion
Let me show you the setup that created this beautiful disaster:
// components/ScrollytellingHero/index.tsx - The beautiful lie
const ScrollytellingHero = () => {
return (
<div className={styles.stickyWrapper}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className={styles.heroContent}
>
{/* Beautiful parallax articles that worked perfectly... on desktop */}
</motion.div>
</div>
);
};
The CSS that I thought was so clever at 2am:
/* styles/scrollytelling.module.css - The innocent-looking culprits */
.stickyWrapper {
position: sticky;
top: 0;
height: 100vh;
/* These two lines would haunt my dreams */
-webkit-transform: translateZ(0); /* "For GPU acceleration!" I thought */
transform: translateZ(0);
will-change: transform; /* "So smooth!" I celebrated */
}
Act 2: Three Days in Mobile Hell
Day 1 (Friday Night): Denial and Chrome DevTools
"It works in Chrome DevTools mobile view!" I insisted to my empty flat at 10pm.
I tested every device in Chrome's emulator. iPhone 12, iPhone 13, iPad, even a Nokia 3310 (kidding, but I would have if I could). Perfect every time. The header appeared right when it should, at exactly 0.75 viewport heights of scroll. The DevTools were lying to me, but I didn't know it yet.
Day 2 (Saturday): The Red Box of Desperation
By Saturday afternoon, I'd added visual debugging directly on the page. This is what desperation looks like in code:
// components/ScrollytellingHero/DebugInfo.tsx - My cry for help
<div style={{
position: 'fixed',
top: 60,
right: 10,
background: 'red',
color: 'white',
padding: '10px',
zIndex: 999999, // Yes, six nines. Don't judge.
fontSize: '12px'
}}>
<div>Scroll: {Math.round(scrollProgress * 100)}%</div>
<div>Header: {showHeader ? 'SHOWING' : 'HIDDEN'}</div>
<div>Time wasted: 14 hours</div> {/* I actually added this */}
</div>
The debug box confirmed my worst fear. The state was correct (showHeader: true
), but the header was nowhere to be seen on actual mobile devices. The JavaScript was working. The CSS had gone rogue.
Day 3 (Sunday): Descent into Madness
By Sunday evening, I'd tried everything. And I mean everything:
Setting z-index to 999999999 (yes, nine nines, because obviously eight wasn't enough). Removing every single animation from the Header component. Adding -webkit-
prefixes to everything, including my coffee mug. Wrapping everything in divs like a CSS burrito. Sacrificing a rubber duck to the CSS gods (it's still on my desk, judging me).
Nothing worked. It was 11pm on Sunday. I'd spent the entire weekend on this. The header remained stubbornly invisible on mobile.
Act 3: The Agent Investigation
Monday morning, 8am. Coffee number one. I was done trying to be clever. Time to bring in the heavy artillery. I deployed a specialised agent with this directive:
# scripts/debug-mobile-header.sh - My white flag of surrender
"Perform a deep dive into why the header isn't showing on mobile
browsers despite the state being correct. Analyse all CSS,
component hierarchies, and mobile-specific behaviours."
Twelve minutes. That's all it took the agent to find what I'd missed for three days.
Finding 1: The Transform Betrayal
/* The agent identified this stacking context trap */
.stickyWrapper {
transform: translateZ(0); /* Creates new stacking context */
}
└── Header {
position: fixed; /* Gets trapped in parent context! */
}
The agent explained it with embarrassing clarity: transforms create a new stacking context, and position: fixed
elements inside become fixed to that context, not the viewport. I wanted to crawl under my desk.
Finding 2: Mobile Safari's Strict Interpretation
Here's where it gets interesting (and by interesting, I mean infuriating). The agent discovered that mobile Safari and Chrome handle stacking contexts differently than desktop browsers.
Desktop browsers, in their infinite mercy, often "help" developers by letting fixed elements escape their transformed parents. They break the CSS specification to save us from ourselves. Mobile browsers follow the spec strictly. No mercy. No escape. Your fixed element stays trapped like a fly in amber.
Finding 3: The Double Whammy
I wasn't just creating a stacking context. I was creating it twice over with both transform
and will-change
. It was like locking a door and then building a wall in front of it. Excessive doesn't begin to describe it.
Act 4: The Embarrassingly Simple Fix
Monday, 11:23am. Armed with understanding, the fix was surgical and humbling:
/* styles/scrollytelling.module.css - Before (Breaking mobile) */
.stickyWrapper {
-webkit-transform: translateZ(0); /* Delete this */
transform: translateZ(0); /* Delete this */
will-change: transform; /* Delete this */
}
/* styles/scrollytelling.module.css - After (Working everywhere) */
.stickyWrapper {
/* Absolutely nothing. The solution was deletion. */
}
/* Only animate what needs animating */
.animatedCard {
transform: translateY(0);
transition: transform 0.3s ease;
}
Eight lines deleted. That's it. Three days of debugging, solved by pressing backspace thirty-seven times.
The Technical Deep Dive (For Fellow Sufferers)
Let me save you three days of your life. Here's everything about stacking contexts you need to know.
What Creates a Stacking Context?
A stacking context is created by any element with: transform
(any value except none), will-change: transform
or will-change: opacity
, filter
(any value except none), perspective
(any value except none), opacity
less than 1, z-index
when position is not static, isolation: isolate
, and about fifteen other properties that will surprise you at the worst possible moment.
Why Mobile Browsers Are Stricter
Mobile browsers optimise aggressively for performance and battery life. When they encounter a stacking context, they treat it as a separate compositing layer, completely isolated from its surroundings. Fixed-position elements inside these layers lose their viewport-relative positioning entirely.
This is actually correct according to the CSS specification. Desktop browsers are technically wrong when they let fixed elements escape. But when desktop browsers are wrong in your favour for years, you start to think that's normal. Mobile browsers gave me a reality check I didn't ask for.
The Aftermath
After fixing the issue on Monday afternoon, the metrics told the story:
Bug reports about missing headers dropped from fifteen to zero. Mobile engagement jumped 47% because users could finally navigate. Performance actually improved after removing the unnecessary transforms (the irony burns). My blood pressure returned to normal levels.
The fix touched just four files, removing more code than it added:
modified: styles/scrollytelling.module.css (-8 lines)
modified: components/ScrollytellingHero/DynamicHeaderFooter.tsx (-2 lines)
modified: styles/globals.css (-3 lines)
modified: components/layout/Header.tsx (-1 line)
Total changes: minus fourteen lines. I fixed three days of pain by deleting code I never needed.
The Final Insult
Once the header was finally visible on mobile, I discovered it appeared too early. Users complained it was "eager" and "pushy" (their words, but I felt them personally). The threshold adjustment took thirty seconds:
// components/ScrollytellingHero/index.tsx - The easiest fix of my life
<DynamicHeaderFooter showThreshold={2.0} /> // Was 0.75
But I never would have gotten to this UX refinement if I hadn't first escaped the stacking context prison. Sometimes you have to fix the foundations before you can paint the walls.
Lessons Learned (The Hard Way)
- •
DevTools lies sometimes - Chrome's mobile emulator doesn't replicate mobile rendering engines. Test on real devices or suffer like I did.
- •
CSS transforms are not free performance - Every transform creates a stacking context that fundamentally changes behaviour. I added
translateZ(0)
for GPU acceleration I didn't need and created a problem I spent three days solving. - •
Read the specification when desperate - Mobile browsers follow the CSS spec. Desktop browsers follow the spec "creatively". Know the difference.
- •
Agents excel at systematic debugging - What took me three days took an agent twelve minutes. My ego is bruised but my respect for systematic analysis has grown.
- •
The best code is deleted code - I fixed a critical bug by removing fourteen lines. Less code, fewer problems.
What This Means for Medical Scrollytelling
Scrollytelling on the web is powerful but fragile, especially for healthcare content where accessibility is paramount. The same CSS properties that enable smooth animations can break fundamental behaviours that users rely on for navigation.
Success requires deep understanding of CSS rendering, not just copying Stack Overflow solutions at 2am. It demands rigorous cross-device testing, especially on the devices your NHS colleagues actually use. Most importantly, it requires the humility to delete your clever optimisations when they cause more problems than they solve.
This three-day journey taught me more about CSS than five years of casual development. Sometimes the best bugs are the ones that force you to truly understand the platform you're building on. Even if they do ruin your weekend.
And yes, the rubber duck is still judging me from my desk. It knows what I did.
Share this article

Dr. Chad Okay
I am a London‑based NHS Resident Doctor with 8+ years' experience in primary care, emergency and intensive care medicine. I'm developing an AI‑native wearable to tackle metabolic disease. I combine bedside insight with end‑to‑end tech skills, from sensor integration to data visualisation, to deliver practical tools that extend healthy years.