useScrollLock
Lock scrolling of the element while preserving scrolling within nested scrollable containers.
useScrollLock prevents scrolling on a target element by setting overflow: hidden on the element’s style. It returns a tuple of the current lock state (boolean) and a setter function to toggle the lock. Crucially, it preserves scrolling within nested scrollable containers inside the locked element, so inner content remains interactive. The lock is automatically released when the component unmounts.
When to Use
- Preventing background scroll when a modal, dialog, or drawer is open
- Locking scroll on the body while displaying a fullscreen overlay or lightbox
- Disabling scroll temporarily during drag-and-drop operations to prevent accidental scrolling
Notes
- SSR-safe: Returns
falsefor the lock state and a no-op setter during server-side rendering. No DOM style manipulation occurs on the server. - Nested scrolling: Unlike naive
overflow: hiddenapproaches, this hook preserves scrolling within child scrollable containers, making it suitable for dialogs with scrollable content. - Related hooks: See also
useScrollfor tracking scroll position,useScrollIntoViewfor programmatic scrolling, anduseFullscreenfor immersive display modes.
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
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
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
| Argument | Description | Type | DefaultValue |
|---|---|---|---|
| target | dom element | BasicTarget<HTMLElement> (Required) | - |
| initialState | default value | 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;