Mastering Scroll Lock: Enhancing User Experience Across Devices

The Problem: Disorienting User Experience

Imagine opening a quick view modal, only to find that the background page moves when you scroll your mouse wheel. This disorienting experience can be frustrating for users, especially when dealing with long content or mobile menus. To avoid this, we need to implement scroll lock.

Implementing Scroll Lock

To update our application and account for users scrolling unexpectedly, we’ll create a Hook, import it into our component, and set up the scroll lock implementation. We’ll start by creating the Hook’s structure:


import { useState, useEffect } from 'eact';

const useScrollLock = () => {
  const [scrollLocked, setScrollLocked] = useState(false);

  // lockScroll and unlockScroll functions will be implemented here
};

We’ll then implement the lockScroll and unlockScroll functions:


const lockScroll = () => {
  // code to lock scroll
};

const unlockScroll = () => {
  // code to unlock scroll
};

Fixing Layout Shift

When the lockScroll function is called, the scrollbar disappears, causing the width of the page to increase and shifting centered content. To prevent this layout shift, we need to compensate for the width of the browser scrollbar. We’ll measure the width of the scrollbar and use this value in our Hook to ensure a seamless user experience.


const getScrollbarWidth = () => {
  const bodyWidth = document.body.offsetWidth;
  document.body.style.overflow = 'hidden';
  const scrollbarWidth = document.body.offsetWidth - bodyWidth;
  document.body.style.overflow = '';
  return scrollbarWidth;
};

Cross-Browser Compatibility

However, we soon realize that the scrollbar width isn’t consistent between browsers, even on the same OS. To tackle this, we’ll dynamically calculate the scrollbar’s width and use it as the padding value on the body element. This ensures that our scroll lock implementation works across different browsers.


useEffect(() => {
  const scrollbarWidth = getScrollbarWidth();
  document.body.style.paddingRight = `${scrollbarWidth}px`;
}, [scrollLocked]);

Handling Sticky Elements

Ecommerce websites often use sticky elements like headers, promo bars, and floating action buttons (FAB). When applying scroll lock, these elements can shift, causing layout issues. To address this, we’ll use our Hook to set a CSS custom property on the body element, which will be used within the styling of any element with a fixed position.


body[scroll-locked] {
  --scroll-lock-padding: 17px; /* calculated scrollbar width */
}

.fixed-element {
  position: fixed;
  right: var(--scroll-lock-padding);
}

iOS Safari and Scroll Lock

Unfortunately, our scroll lock function doesn’t work on iOS devices. To solve this, we’ll handle iOS specifically using a user agent sniff and an adaptation of an approach originally presented by Markus Oberlehner. This involves setting the body to position='fixed' and programmatically offsetting the body to match the current scroll distance.


const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);

if (isIOS) {
  document.body.style.position = 'fixed';
  document.body.style.top = `-${window.scrollY}px`;
}

The Result: A Seamless User Experience

By implementing scroll lock and addressing the challenges that come with it, we can ensure a seamless user experience across devices. Our completed Hook will help prevent users from getting disoriented, lost, or frustrated, ultimately leading to a better interaction with our website.

Leave a Reply