Skip to content

Improved mobile toolbar positioning using CSS variables instead of React state #2615

@Movm

Description

@Movm

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:

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.clientHeight instead of window.innerHeight to avoid the URL bar inflating the keyboard height calculation on Android Chrome
  • Listens to both resize and scroll events on visualViewport — 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions