How to keep the caret at a distance from the bottom of the window?

(If it’s not OK to ask this here @marijn please let me know and I’ll delete this thread)

Whenever the contenteditable element grows past the height of the window, browsers strive to keep the caret very close to the bottom of the window. This is annoying from a UX perspective in longer documents because users end up always spending their time at the very bottom of the window.

To solve this, I’m trying to find a way so that the caret never reaches the very bottom of the window.

This is how Google Docs does it:

And this is Medium:

Has anyone found an elegant way to solve this?

I’ve tried with CSS margins and paddings but it doesn’t work. I imagine I can hack my way by manipulating the scroll position but I’d rather avoid that if possible.

I solved it using EditorView.coordsAtPos().

Here’s a naive implementation of a plugin that solves this:

import {Plugin} from 'prosemirror-state'

export default (callback) => {
  return new Plugin({
    view() {
      return {
        update (updatedView) {
          const caretPosition = updatedView.state.selection.head;
          const rect = updatedView.coordsAtPos(caretPosition);
          const caretY = rect.y || rect.top;
          const limitY = document.documentElement.clientHeight - 100;

          if (caretY >= limitY) {
            setTimeout(() => {
              window.scrollTo({
                top: window.scrollY + 250,
                behavior: 'smooth'
              });
            }, 50);
          }
        }
      }
    }
  });
};

The setTimeout is necessary because if the browser is already scrolling then scrollTo won’t work. This happens when pasting text which moves the caret below the limit of the window.

Obviously a better implementation will use throttling/debouncing and also wait until the browser has stopped scrolling.

1 Like

I think you should also be able to this with the scrollThreshold prop.

1 Like

Thanks @marijn !

I actually had to use a combination of scrollThreshold and scrollMargin to achieve the behavior I wanted.

view = new EditorView(element, {state, scrollMargin: 200, scrollThreshold: 200});

When using scrollThreshold alone it didn’t work when pasting text or pressing enter, only when navigating with the down arrow key.