useScrollLock
锁定元素的滚动行为,同时保留嵌套可滚动容器内的滚动功能。
useScrollLock 通过在元素样式上设置 overflow: hidden 来阻止目标元素的滚动。它返回一个包含当前锁定状态(布尔值)和切换锁定的设置函数的元组。重要的是,它保留了锁定元素内嵌套可滚动容器的滚动,因此内部内容仍然可交互。锁定在组件卸载时自动释放。
使用场景
- 当模态框、对话框或抽屉打开时阻止背景滚动
- 在显示全屏覆盖层或灯箱时锁定 body 的滚动
- 在拖放操作期间临时禁用滚动以防止意外滚动
注意事项
- 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
| 参数名 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| target | dom元素 | BasicTarget<HTMLElement> (必填) | - |
| initialState | 默认值 | boolean | undefined | false |
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;