Have you ever been surprised that CSS isn't working the way you expected?
That happened to me (again) when I set an element to be fixed at the bottom and then opened the keyboard on my iPhone.
<div className="fixed bottom-0" />
What I've seen is that element is not visible at all.
Because it's fixed. To the bottom. Behind the keyboard.
Seems like to fix the fixed
we need some JS.
There's a browser API with good support that can be used for these purposes: VisualViewport.
It returns the width and height of the actual visible viewport. MDN link to docs.
However, do your own investigation to see if it's supported for the versions you're targeting.
Basically, we need to handle the position of the element with respect to the visual viewport, as well as the scroll position and the element's height. Let's do the math.
Also, since the math is much simpler this way, it makes sense to use the top parameter instead of the bottom.
top = viewport height + scroll - element height
I'll use React. For any other framework, you can just copy the content of the useEffect
hook.
import { useEffect, useState } from 'react';
import classNames from 'classnames';
import { useDebounce } from 'use-debounce';
const elementHeight = 55; // elem. height in pixels
// It's also a good idea to calculate it dynamically via ref
export const FixedBlock = () => {
// top postion -> the most important math result goes here
const [top, setTop] = useState(0);
useEffect(() => {
function resizeHandler() {
// viewport height
const viewportHeight = window.visualViewport?.height ?? 0;
// math
setTop(viewportHeight + window.scrollY - elementHeight)
}
// run first time to initialize
resizeHandler();
// subscribe to events which affect scroll, or viewport position
window.visualViewport?.addEventListener('resize', resizeHandler);
window.visualViewport?.addEventListener('scroll', resizeHandler);
window?.addEventListener('touchmove', resizeHandler);
// unsubscribe
return () => {
window.visualViewport?.removeEventListener('resize', resizeHandler);
window.visualViewport?.removeEventListener('scroll', resizeHandler);
window?.removeEventListener('touchmove', resizeHandler);
};
}, [debouncedScroll]);
return (
<>
<div
className={classNames(
'absolute left-0 top-0', // <-- attention, it's absolute
top === 0 && 'hidden' // while calculating, we don't need to show it
)}
style={{ transform: `translateY(${debouncedTop}px)` }}
>
I am fixed
</div>
</>
);
};