-
-
Notifications
You must be signed in to change notification settings - Fork 709
Improved mobile toolbar positioning using CSS variables instead of React state #2615
Description
Problem
The current ExperimentalMobileFormattingToolbarController uses React state (setTransform) on every visualViewport scroll/resize event. Each state update triggers a full React re-render, causing visible flickering as noted in the code comments and in #1284.
Additionally, related mobile toolbar issues exist:
- Format toolbar goes offscreen on mobile #938 — Format toolbar goes offscreen on mobile
- iOS Safari: On text selection two overlapping menus #2122 — iOS Safari: overlapping menus on text selection
Proposed Solution
We implemented an alternative approach in our project that eliminates the flickering by using CSS custom properties instead of React state for positioning:
useEffect(() => {
if (!window.visualViewport) return;
const vp = window.visualViewport;
const update = () => {
const layoutHeight = document.documentElement.clientHeight;
const keyboardHeight = layoutHeight - vp.height;
wrapperRef.current?.style.setProperty(
'--mobile-keyboard-offset',
keyboardHeight > 0 ? `${keyboardHeight}px` : '0px'
);
};
vp.addEventListener('resize', update);
vp.addEventListener('scroll', update);
return () => {
vp.removeEventListener('resize', update);
vp.removeEventListener('scroll', update);
};
}, []);Combined with CSS:
@media (max-width: 767px) {
.formatting-toolbar {
position: fixed;
bottom: var(--mobile-keyboard-offset, 0px);
left: 0;
right: 0;
padding-bottom: calc(0.375rem + env(safe-area-inset-bottom, 0));
z-index: 50;
}
}Why this works better
| Aspect | Current (React state) | Proposed (CSS variable) |
|---|---|---|
| Re-renders on keyboard change | Yes — setTransform triggers reconciliation |
No — style.setProperty is direct DOM |
| Flickering | Visible due to render cycle delay | None — compositor handles CSS vars |
| URL bar handling | Uses window.innerHeight (includes URL bar) |
Uses clientHeight (excludes URL bar) |
| Safe area (notch) | Not handled | env(safe-area-inset-bottom) |
| Scroll events | Not listened to | Listened — handles URL bar collapse |
Key implementation details
- Uses
document.documentElement.clientHeightinstead ofwindow.innerHeightto avoid the URL bar inflating the keyboard height calculation on Android Chrome - Listens to both
resizeandscrollevents onvisualViewport— the scroll event fires when the URL bar collapses/expands env(safe-area-inset-bottom)handles iPhone notch/home indicator- Touch device detection gates the listener so desktop is unaffected
Willingness to contribute
We'd be happy to submit a PR with a MobileFormattingToolbarController component implementing this approach. We're using it in production with BlockNote v0.47.3 and it works well across iOS Safari and Android Chrome.
Would love feedback on whether this direction makes sense before writing the PR. Happy to coordinate with #2591 (portal floating UI to body) if needed.