useScrollLock

鎖定元素的滾動行為,同時保留嵌套可滾動容器內的滾動功能。

useScrollLock 控制頁面或容器的捲動鎖定,防止使用者捲動。它回傳一個布林值元組,包含當前鎖定狀態和切換鎖定的函式。此 hook 在鎖定捲動的同時保留子容器的捲動能力,適合用於對話框和模態框。

使用場景

  • 開啟模態框或側邊欄時鎖定背景捲動
  • 在特定使用者互動期間防止頁面捲動
  • 建構全螢幕覆蓋層時防止底層內容捲動

注意事項

  • SSR 安全:在伺服器端渲染時鎖定狀態回傳 false,設定函式為空操作。伺服器上不會進行 DOM 樣式操作。
  • 巢狀捲動:與簡單的 overflow: hidden 方法不同,此 hook 保留了子可捲動容器內的捲動,使其適用於包含可捲動內容的對話框。
  • 相關 hooks:另請參閱 useScroll 追蹤捲動位置,useScrollIntoView 用於程式化捲動,以及 useFullscreen 用於沉浸式顯示模式。

基礎用法

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",
            }}
          >
            左上角
          </div>
          <div
            style={{
              ...absoluteStyle,
              bottom: "0rem",
              left: "0rem",
            }}
          >
            左下角
          </div>
          <div
            style={{
              ...absoluteStyle,
              top: "0rem",
              right: "0rem",
            }}
          >
            右上角
          </div>
          <div
            style={{
              ...absoluteStyle,
              bottom: "0rem",
              right: "0rem",
            }}
          >
            右下角
          </div>
          <div
            style={{
              ...absoluteStyle,
              top: "33.33333%",
              left: "33.33333%",
            }}
          >
            滾動區域
          </div>
        </div>
      </div>
      <div
        style={{
          width: 280,
          margin: "auto",
          paddingLeft: "1rem",
          display: "flex",
          flexDirection: "column",
          gap: 5,
        }}
      >
        <div>鎖定狀態: {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 ? "解鎖" : "鎖定"}
        </button>
      </div>
    </div>
  );
};
Result

對話框示例

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)" }}>
              可滾動內容的對話框
            </h3>
            <p style={{ lineHeight: "1.6", color: "var(--ifm-color-content-secondary)", margin: "16px 0" }}>
              這個對話框演示了 useScrollLock 的使用。背景滾動被鎖定,
              但你仍然可以在對話框內容中正常滾動。
            </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",
                }}
              >
                關閉
              </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",
            }}
          >
            打開對話框
          </button>
        } 
      />
      <p style={{ 
        marginTop: "16px", 
        color: "var(--ifm-color-content-secondary)", 
        fontSize: "14px",
        maxWidth: "400px",
        margin: "16px auto 0"
      }}>
        打開對話框并尝试滾動。背景滾動將被鎖定,
        但你可以在對話框內容中正常滾動。
      </p>
    </div>
  );
}
Result

API

useScrollLock

Returns

readonly [boolean, (flag: boolean) => void]: 包含以下元素的元組:

  • 是否鎖定。
  • 更新鎖定值的函數。

Arguments

參數名描述類型預設值
targetdom元素BasicTarget<HTMLElement> (必填)-
initialState默认值boolean | 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;