Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →For a modal, drawer, lightbox, or full-screen menu, lock the document root while leaving the overlay’s content scrollable:
html.is-scroll-locked,
body.is-scroll-locked {
overflow: hidden;
}
Add the class to both <html> and <body> when the overlay opens, then remove it when the overlay closes. Use overflow: clip instead of hidden when script-driven and focus-driven scrolling must be blocked too. The sections below show a complete implementation, mobile and accessibility safeguards, and a troubleshooting path.
What the scroll lock must do
A page lock has two separate jobs:
- Stop the document itself from moving when the overlay is open.
- Keep the overlay usable if its content is taller than the viewport.
Applying overflow only to a visible panel does not stop the underlying document. For a page-level lock, target the root document by applying a temporary state to both html and body. A class is preferable to permanently rewriting inline styles because opening and closing the component becomes an explicit state change.
CSS-first page locking
Use hidden for the usual modal case
html.is-scroll-locked,
body.is-scroll-locked {
overflow: hidden;
}
overflow: hidden clips overflowing content and removes the visible scrollbar. It does not, however, create an absolute prohibition on movement: focus navigation, scrollTop, and scrollTo() can still bring content into view. That behavior is often desirable when keyboard focus must move to a control inside the locked region.
#1 Best Overall
Use clip for a harder lock
html.is-scroll-locked,
body.is-scroll-locked {
overflow: clip;
}
clip does not create a scroll container and does not support programmatic scrolling. Choose it when the requirement is to prevent both user scrolling and script-controlled movement. Test the browsers you support before making it your default, especially if your component relies on focus-driven scrolling.
| Property | User wheel or touch scroll | Programmatic scroll and focus movement | Best fit |
|---|---|---|---|
hidden |
Blocked while overflow is clipped | Can still move content in some cases | Most dialogs and drawers |
clip |
Blocked | Not supported for the clipped element | Hard locks where no movement is acceptable |
Keep the modal or drawer scrollable
Do not lock every ancestor of the overlay. Give the dialog’s content a bounded block size and its own scrolling area:
.dialog {
max-block-size: 90vh;
overflow: auto;
overscroll-behavior: contain;
}
max-block-size: 90vh keeps the panel inside the viewport in a typical horizontal writing mode. overflow: auto lets users read long content. overscroll-behavior: contain prevents scroll chaining when the panel reaches its top or bottom, so a swipe at the boundary does not start moving the page behind it. Use overscroll-behavior: none when you also want to suppress the browser’s default boundary effect.
Toggle the lock with JavaScript
The smallest reliable API adds and removes one class on each root element:
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →function lockPage() {
document.documentElement.classList.add('is-scroll-locked');
document.body.classList.add('is-scroll-locked');
}
function unlockPage() {
document.documentElement.classList.remove('is-scroll-locked');
document.body.classList.remove('is-scroll-locked');
}
Call lockPage() when the modal becomes visible and unlockPage() in every close path: the close button, Escape handling, backdrop dismissal, route changes, and component unmount. Removing your class leaves any pre-existing inline overflow values and unrelated classes untouched. If another part of your application also controls overflow, coordinate the states rather than assigning auto during cleanup.
Make repeated opens and closes safe
For nested dialogs or multiple components, track ownership so one component cannot unlock a page still needed by another. A simple counter works when dialogs cannot be independently reordered:
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
let lockCount = 0;
function acquirePageLock() {
lockCount += 1;
if (lockCount === 1) {
document.documentElement.classList.add('is-scroll-locked');
document.body.classList.add('is-scroll-locked');
}
}
function releasePageLock() {
if (lockCount === 0) return;
lockCount -= 1;
if (lockCount === 0) {
document.documentElement.classList.remove('is-scroll-locked');
document.body.classList.remove('is-scroll-locked');
}
}
Use a component-level manager instead if overlays can close out of order. The important rule is that cleanup must restore the state that existed before the lock.
Prevent scroll chaining at the boundary
Without containment, a user can scroll a long modal to its end and continue dragging, causing the page underneath to move. Put the containment rule on the actual scrolling element, not only on the backdrop:
Free tools Windows power users keep installed
One-click scans. No signup required.
.dialog__content {
max-height: min(90vh, 48rem);
overflow-y: auto;
overscroll-behavior-y: contain;
}
Use overscroll-behavior-y when horizontal gestures should retain their normal behavior. For a full suppression of boundary effects, change the value to none. This CSS approach is preferable to cancelling every touch event because it leaves ordinary page gestures alone when the dialog is closed.
When JavaScript event cancellation is necessary
Some touch or wheel interactions still need cancellation in a particular component. Add narrowly scoped listeners only while the lock is active:
const cancelScroll = event => event.preventDefault();
function lockWithEvents() {
document.addEventListener('wheel', cancelScroll, { passive: false });
document.addEventListener('touchmove', cancelScroll, { passive: false });
}
function unlockWithEvents() {
document.removeEventListener('wheel', cancelScroll);
document.removeEventListener('touchmove', cancelScroll);
}
The passive: false option is required because the handler calls preventDefault(). Register the exact same function reference that you remove. Do not leave these listeners installed globally: otherwise normal page scrolling, embedded widgets, and other components can remain broken after the overlay closes.
Prefer CSS for the normal lock and add event cancellation only for a demonstrated device or component problem. A non-cancelable event cannot be stopped with preventDefault(), so inspect the event type and browser behavior before adding a listener.
Rank #3
Accessibility and focus behavior
Stopping movement does not make a modal accessible. While it is open:
- Use a dialog with an accessible name, such as
role="dialog",aria-modal="true", and a visible heading referenced byaria-labelledby. - Move focus to the dialog’s heading or first usable control.
- Keep keyboard focus inside the open dialog; a focus trap or the native
<dialog>element can provide this behavior. - Provide a visible close button and support Escape where appropriate.
- Return focus to the control that opened the dialog after closing.
- Restore the page lock even when the close operation is triggered by navigation or an exception.
With hidden, a browser may still scroll an element into view when focus moves to it. That can be useful for keyboard users. With clip, the clipped element is not a scroll container, so verify that your focus strategy does not hide the active control.
Scrollbar layout shifts
Removing the document scrollbar can increase the available layout width. Fixed headers, centered content, and elements aligned to the right edge may visibly jump when the lock is applied. Test the open and closed states at the target viewport sizes. If stable geometry is required, reserve the scrollbar gap in your layout or apply a measured compensation only while the lock class is present. Do not hard-code a universal pixel value: scrollbar widths vary by operating system, browser settings, and overlay-scrollbar configuration.
Complete modal example
This standalone example locks the page, keeps the dialog content scrollable, supports Escape, and restores focus:
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errors<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Scroll lock demo</title>
<style>
html.is-scroll-locked,
body.is-scroll-locked { overflow: hidden; }
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; }
main { min-height: 180vh; padding: 2rem; }
.backdrop {
position: fixed; inset: 0; display: grid; place-items: center;
padding: 1rem; background: rgb(0 0 0 / .55);
}
.dialog {
width: min(42rem, 100%); max-block-size: 90vh;
overflow: auto; overscroll-behavior: contain;
padding: 1.5rem; background: white; border-radius: .5rem;
}
[hidden] { display: none; }
</style>
</head>
<body>
<main>
<button id='open' type='button'>Open dialog</button>
<p>Add enough page content to make the document scrollable.</p>
</main>
<div id='backdrop' class='backdrop' hidden>
<section class='dialog' role='dialog' aria-modal='true'
aria-labelledby='dialog-title' tabindex='-1'>
<h2 id='dialog-title'>Scrollable dialog</h2>
<p>Long content stays scrollable inside this panel while the page is locked.</p>
<p>Repeat content as needed for your application.</p>
<button id='close' type='button'>Close</button>
</section>
</div>
<script>
const openButton = document.querySelector('#open');
const closeButton = document.querySelector('#close');
const backdrop = document.querySelector('#backdrop');
const dialog = document.querySelector('.dialog');
let previousFocus;
function openDialog() {
previousFocus = document.activeElement;
backdrop.hidden = false;
document.documentElement.classList.add('is-scroll-locked');
document.body.classList.add('is-scroll-locked');
closeButton.focus();
}
function closeDialog() {
backdrop.hidden = true;
document.documentElement.classList.remove('is-scroll-locked');
document.body.classList.remove('is-scroll-locked');
if (previousFocus instanceof HTMLElement) previousFocus.focus();
}
openButton.addEventListener('click', openDialog);
closeButton.addEventListener('click', closeDialog);
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && !backdrop.hidden) closeDialog();
});
backdrop.addEventListener('click', event => {
if (event.target === backdrop) closeDialog();
});
</script>
</body>
</html>
The sample demonstrates locking, but a production focus trap should cycle Tab and Shift+Tab within the dialog or use a well-tested accessibility primitive. If you use the native <dialog> element, verify how your target browsers handle its modal behavior and still test focus restoration.
Mobile and touch testing checklist
- Open the overlay at the top, middle, and bottom of a long page.
- Swipe inside a short panel, a long panel, and directly on the backdrop.
- Test iOS Safari and Android browsers with both touch and an attached keyboard.
- Check pull-to-refresh and edge-swipe behavior; use
overscroll-behavioronly where the design calls for containment. - Rotate the device and resize the viewport while the overlay is open.
- Close through every route and confirm the page scrolls normally afterward.
- Verify that screen-reader focus, keyboard focus, and visible focus indicators remain inside the modal until it closes.
Common failures and fixes
The page still scrolls
Confirm that the class is on both document.documentElement and document.body, that the class name matches the CSS exactly, and that no later rule overrides overflow. Check for an inline style or a framework utility with higher specificity.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
The modal cannot be read
The page may be locked correctly while the panel has no bounded size. Add max-height or max-block-size and overflow: auto to the panel’s content area.
Closing leaves scrolling disabled
Look for an early return, a route transition, or an exception that skips cleanup. Centralize unlock logic and make it idempotent. If locks can nest, use ownership or a counter instead of removing the class unconditionally.
The page jumps sideways
The scrollbar disappeared and the viewport widened. Reserve or compensate for the scrollbar gap only during the locked state, and test on systems with overlay scrollbars as well as classic scrollbars.
Touch scrolling is inconsistent
Start with root overflow and panel overscroll-behavior. If a specific browser still scrolls the page, add temporary wheel or touchmove listeners with passive: false, then remove them during unlock. Avoid permanent document-level cancellation.
Focus moves behind the modal
Apply a focus trap or native modal dialog behavior, mark the dialog with an accessible name, and return focus to the opener. Do not rely on clipping as an accessibility mechanism.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Performance and reliability notes
Changing a class on the root is inexpensive; the visible work is usually layout recalculation caused by scrollbar removal. Keep the lock transition separate from large DOM mutations, and avoid repeatedly adding and removing listeners on every pointer event. A single open and close lifecycle is easier to audit than scattered style assignments.
Best Value
Choose hidden when focus navigation or controlled scripts must still reach content, and clip when the locked element must not scroll programmatically. Whichever value you choose, test the exact combination of browser, operating system, input method, and overlay implementation that your users receive.
Or skip the browser setup
If you need a clean image of the page after implementing the lock, ScreenshotNeo can capture it through one request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
Use the API examples in the ScreenshotNeo documentation with your deployed URL:
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper and page-range controls, HTML/CSS-to-image, custom JavaScript and CSS, pre-capture clicks, hidden selectors, selector or network-idle waits, ad and tracker blocking, custom headers, cookies, user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, an OpenAPI specification, and compatible parameter names used by other screenshot APIs.
Recommended Free Tools
The Free plan includes 1,000 screenshots per month with no card. Paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing provides two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to start.
FAQ
Can CSS lock only vertical scrolling?
Yes. Apply overflow-y: hidden or overflow-y: clip to the locked roots while leaving horizontal overflow under a deliberate policy. Test wide tables, zoomed pages, and right-to-left layouts because horizontal movement may still expose content outside the viewport.
Does a parent page lock stop an iframe from scrolling?
No. A cross-origin iframe owns its own document and scroll state. You need cooperation from the framed page, or you must size the iframe so it does not create an inner scrolling region. For a same-origin iframe, apply the lock inside that document as well.
Why does a keyboard user sometimes move the page even when it is locked?
hidden can still allow focus-driven movement, and keyboard scrolling can target a different scroll container. Keep focus inside the modal, decide whether that behavior is acceptable, and choose clip only when suppressing programmatic movement is more important than that focus behavior.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




