DEV Community

Cauê Nolasco
Cauê Nolasco

Posted on

React Typescript Code Snippet: How to smoothly scroll + focus to invalid input on form validation?

When we use onSubmit, the default behaviour is to instant scroll and focus invalid element. Sometimes, if you have fixed headers, the input becomes hidden after scroll.

The following function will:

  1. Select the first errored input
  2. Smoothly scroll to selected input
  3. Apply offset from top
  4. Set focus, when input becomes visible
// Declare a window variable to get only first invocation of 'onInvalid' from 'form' 
declare global {
  interface Window {
    scrolled?: boolean;
  }
}
export function scrollToElement(
  element: EventTarget & HTMLFormElement,
  offset: number = 250
) {
  // It uses offset to check if its visible below header
  function isFullVisible(element: EventTarget & HTMLFormElement, offset: number = 0) {
    const elementRect = element.getBoundingClientRect();
    const windowHeight =
      (window.innerHeight || document.documentElement.clientHeight);

      const isTopVisible = elementRect.top >= offset && elementRect.top < windowHeight;
      const isBottomVisible =
      elementRect.bottom >= offset && elementRect.bottom <= windowHeight;

      const fullVisible = isTopVisible && isBottomVisible;
    return fullVisible
  }

  function run(element: EventTarget & HTMLFormElement, offset: number = 250) {
    const elementPosition =
      element.getBoundingClientRect().top + window.scrollY;

    const alreadyRunt = window.scrolled;

    if (alreadyRunt) {
      return;
    } else {
      window.scrolled = true;
      // Time to guarantee that all invocations of 'onInvalid' was ended
      setTimeout(() => delete window["scrolled"], 300);
    }

    if (isFullVisible(element, offset)) {
      element?.focus();
      return;
    }

    window.scroll({
      // This scroll is not always accurate
      // -1 is to force a scroll if by chance it is not visible for less than 1px
      top: elementPosition - offset - 1,
      behavior: "smooth",
    });

    window.addEventListener("scroll", function scrollHandler() {
      // 0 offset to immediate focus on user view, and not when scroll ended
      const fullVisible = isFullVisible(element, 0);

      if (fullVisible) {
        window.removeEventListener("scroll", scrollHandler);
        element?.focus();
      }
    });
  }

  return run(element, offset);
}

Enter fullscreen mode Exit fullscreen mode

Then just use it

<form
  onSubmit={(e) => {
    // ...
  }}
  onInvalid={(e) => {
    e.preventDefault()

    const element = e.target as EventTarget & HTMLFormElement
    scrollToElement(element, 250) // 250 is header height + gap
  }}
>
   ...
Enter fullscreen mode Exit fullscreen mode

This post was created with the aim of helping people through the search, and registering this snippet for my future uses.

I hope you found this, and it helped you. :D

Top comments (0)