React: fixed block isn’t repainting in Mobile Safari

Faced up with problem, shown in this article: https://remysharp.com/2012/05/24/issues-with-position-fixed-scrolling-on-ios#scrolling–unusable-positionfixed-element in Mobile Safari.

I’d caught this bug while coding the footer. My code structure is:

<div>
    ...
    <div className="wrap">
        <div className="footer">
            { ... content of tabs }
        </div>
    </div>
</div>

And the styles:

.wrap { 
    height: 60px;
    position: relative;
    z-index: 100;
}

.footer {
    position: fixed;
    bottom: 0;
    left: 0;
    height: 60px;
    width: 100%;
    z-index: 200;
    display: flex;
    background: #f2f2f7;
    border-top: 1px solid #f0f0f0;
}

Wanna say about solution. Maybe it will be helpful for someone.

I’ve tried solutions from the issues:

But it didn’t look good and my footer was flickering. Sometimes (using very-very slow scroll) bug happened again.

I’ve also tried use “sticky” styles instead of “fixed”, for wrapper or footer, but it also didn’t work.

Finally, I found the solution:

  1. I added listener on scroll event by js and changed styles.
    The idea is: until the scrolling ends, I add “sticky” class to the parent; when the scrolling ends, I remove it.
    I’ve seen similar issue with header here: React: Sticky header flickers in iOS Safari
  2. I added “will-change: auto” styles to the wrapper.
    I found this idea from the comment: https://stackoverflow.com/a/51940706/22479266

The magic is that these fixes don’t work on their own, only using them both.

The final solution looks like:

const ref = useRef(null);

const handleScroll = useCallback(() => {
    if (ref.current) {
        if (ref.current.getBoundingClientRect().bottom > window.innerHeight) {
            if (!ref.current.classList.contains("sticky")) {
                ref.current.classList.add("sticky");
            }
        } else {
            ref.current.classList.remove("sticky");
        }
    }
}, []);

useEffect(() => {
    document.addEventListener('scroll', handleScroll);

    return () => {
        document.removeEventListener('scroll', handleScroll);
    }
}, [handleScroll]);

return <div className="wrap" ref={ref}>
    <div className="footer">
        { ... content of tabs }
    </div>
</div>

And the styles are:

.wrap {
    display: block;
    width: 100%;
    position: relative;
    z-index: 100;
    height: calc(60px + env(safe-area-inset-bottom));
    will-change: auto;
}

.footer {
    bottom: 0;
    left: 0;
    position: fixed;
    right: 0;
    height: calc(60px + env(safe-area-inset-bottom));
    z-index: 200;
    width: 100%;
    display: flex;
    background: #f2f2f7;
    border-top: 1px solid #f0f0f0;
}

If anyone understands why this helps, please, tell me.