Skip to main content

useScrollLock

Lock scrolling of the element while preserving scrolling within nested scrollable containers.

Usage

Live Editor
function Demo() {
  const elementRef = useRef<HTMLDivElement>(null);
  const [locked, setLocked] = useScrollLock(elementRef);

  const absoluteStyle: CSSProperties = {
    paddingTop: "0.25rem",
    paddingBottom: "0.25rem",
    paddingLeft: "0.5rem",
    paddingRight: "0.5rem",
    position: "absolute",
  };
  return (
    <div style={{ display: "flex" }}>
      <div
        ref={elementRef}
        style={{
          width: 300,
          height: 300,
          margin: "auto",
          borderRadius: "0.25rem",
          overflow: "scroll",
          border: "1px solid var(--ifm-color-emphasis-200)",
        }}
      >
        <div style={{ width: 500, height: 400, position: "relative" }}>
          <div
            style={{
              ...absoluteStyle,
              top: "0rem",
              left: "0rem",
            }}
          >
            TopLeft
          </div>
          <div
            style={{
              ...absoluteStyle,
              bottom: "0rem",
              left: "0rem",
            }}
          >
            BottomLeft
          </div>
          <div
            style={{
              ...absoluteStyle,
              top: "0rem",
              right: "0rem",
            }}
          >
            TopRight
          </div>
          <div
            style={{
              ...absoluteStyle,
              bottom: "0rem",
              right: "0rem",
            }}
          >
            BottomRight
          </div>
          <div
            style={{
              ...absoluteStyle,
              top: "33.33333%",
              left: "33.33333%",
            }}
          >
            Scroll Me
          </div>
        </div>
      </div>
      <div
        style={{
          width: 280,
          margin: "auto",
          paddingLeft: "1rem",
          display: "flex",
          flexDirection: "column",
          gap: 5,
        }}
      >
        <div>Locked: {JSON.stringify(locked)}</div>
        <button
          onClick={() => {
            setLocked(!locked);
          }}
          style={{
            padding: "8px 16px",
            border: "1px solid var(--ifm-color-emphasis-300)",
            backgroundColor: "var(--ifm-background-color)",
            color: "var(--ifm-color-content)",
            borderRadius: "4px",
            cursor: "pointer",
          }}
        >
          {locked ? "Unlock" : "Lock"}
        </button>
      </div>
    </div>
  );
};

Result
TopLeft
BottomLeft
TopRight
BottomRight
Scroll Me
Locked: false

Dialog Example

Live Editor
function DialogDemo() {
  const { cloneElement, ReactElement, useRef, useState } = React;
  
  const Dialog = ({ trigger }: { trigger: ReactElement }) => {
    const dialogRef = useRef<HTMLDialogElement>(null);
    const boxRef = useRef(null);
    const [isOpen, setIsOpen] = useState(false);
    const [, setLock] = useScrollLock(document.body);

    const open = () => {
      setIsOpen(true);
      dialogRef.current?.showModal();
      setLock(true);
    };

    const close = () => {
      setIsOpen(false);
      dialogRef.current?.close();
      setLock(false);
    };

    useEventListener("close", () => setIsOpen(false), dialogRef);
    useClickOutside(boxRef, close);

    return (
      <>
        {cloneElement(trigger, { onClick: open })}

        <dialog 
          ref={dialogRef}
          style={{
            padding: 0,
            border: "none",
            borderRadius: "8px",
            boxShadow: "0 10px 25px rgba(0, 0, 0, 0.1)",
            maxWidth: "500px",
            width: "90vw",
          }}
        >
          <div 
            ref={boxRef}
            style={{
              padding: "24px",
              maxHeight: "70vh",
              overflow: "auto",
              backgroundColor: "var(--ifm-background-color)",
              borderRadius: "8px",
            }}
          >
            <h3 style={{ margin: "0 0 16px 0", color: "var(--ifm-color-content)" }}>
              Dialog with Scrollable Content
            </h3>
            <p style={{ lineHeight: "1.6", color: "var(--ifm-color-content-secondary)", margin: "16px 0" }}>
              This dialog demonstrates how useScrollLock works. 
              The background scrolling is locked, but you can still scroll within this dialog content.
            </p>
            <p style={{ lineHeight: "1.6", color: "var(--ifm-color-content-secondary)", margin: "16px 0" }}>
              Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque
              faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi
              pretium tellus duis convallis. Tempus leo eu aenean sed diam urna
              tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas.
              Iaculis massa nisl malesuada lacinia integer nunc posuere.
            </p>
            <p style={{ lineHeight: "1.6", color: "var(--ifm-color-content-secondary)", margin: "16px 0" }}>
              Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora
              torquent per conubia nostra inceptos himenaeos. Lorem ipsum dolor 
              sit amet consectetur adipiscing elit. Quisque faucibus ex sapien 
              vitae pellentesque sem placerat.
            </p>
            <p style={{ lineHeight: "1.6", color: "var(--ifm-color-content-secondary)", margin: "16px 0" }}>
              In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean 
              sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus 
              bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere.
            </p>
            <p style={{ lineHeight: "1.6", color: "var(--ifm-color-content-secondary)", margin: "16px 0" }}>
              Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora
              torquent per conubia nostra inceptos himenaeos. Sed cursus turpis 
              a purus aliquam condimentum. Nam bibendum cursus dolor.
            </p>
            <div style={{ 
              display: "flex", 
              justifyContent: "flex-end", 
              marginTop: "24px",
              gap: "12px"
            }}>
              <button
                onClick={close}
                style={{
                  padding: "8px 16px",
                  border: "1px solid var(--ifm-color-emphasis-300)",
                  backgroundColor: "var(--ifm-background-color)",
                  color: "var(--ifm-color-content)",
                  borderRadius: "4px",
                  cursor: "pointer",
                }}
              >
                Close
              </button>
            </div>
          </div>
        </dialog>
      </>
    );
  };

  return (
    <div style={{ padding: "20px", textAlign: "center" }}>
      <Dialog 
        trigger={
          <button
            style={{
              padding: "12px 24px",
              border: "1px solid var(--ifm-color-primary)",
              backgroundColor: "var(--ifm-color-primary)",
              color: "var(--ifm-color-white)",
              borderRadius: "4px",
              cursor: "pointer",
              fontSize: "16px",
            }}
          >
            Open Dialog
          </button>
        } 
      />
      <p style={{ 
        marginTop: "16px", 
        color: "var(--ifm-color-content-secondary)", 
        fontSize: "14px",
        maxWidth: "400px",
        margin: "16px auto 0"
      }}>
        Open the dialog and try scrolling. You'll be able to scroll 
        within the dialog content while background scrolling remains locked.
      </p>
    </div>
  );
}

Result

Dialog with Scrollable Content

This dialog demonstrates how useScrollLock works. The background scrolling is locked, but you can still scroll within this dialog content.

Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere.

Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat.

In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere.

Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. Sed cursus turpis a purus aliquam condimentum. Nam bibendum cursus dolor.

Open the dialog and try scrolling. You'll be able to scroll within the dialog content while background scrolling remains locked.

API

useScrollLock

Returns

readonly [boolean, (flag: boolean) => void]: A tuple with the following elements:

  • whether scroll is locked.
  • A function to update the value of lock state.

Arguments

ArgumentDescriptionTypeDefaultValue
targetdom elementBasicTarget<HTMLElement> (Required)-
initialStatedefault valueboolean | undefinedfalse

BasicTarget

export type BasicTarget<T extends TargetType = Element> = (() => TargetValue<T>) | TargetValue<T> | MutableRefObject<TargetValue<T>>;

TargetValue

type TargetValue<T> = T | undefined | null;

TargetType

type TargetType = HTMLElement | Element | Window | Document | EventTarget;
Squarespace
Squarespace makes your marketing, customer management, and checkout flow effortless—all on a single platform.
Get Started