Skip to main content

How to Work with Window Sizes and Scrolling in JavaScript

The previous guide covered measuring individual elements. But web development also requires measuring the browser window itself and controlling how the user scrolls through the page. You need to know how wide and tall the viewport is, how big the entire document is (including the parts scrolled out of view), where the user is currently scrolled to, and how to scroll programmatically to specific positions or elements.

These capabilities power some of the most common web interactions: sticky headers that appear after scrolling down, infinite scroll feeds, "back to top" buttons, smooth scroll navigation, scroll progress indicators, lazy loading of images, and responsive layout adjustments based on viewport size.

This guide covers every window-level measurement and scrolling method available in JavaScript. You will learn the difference between the viewport and the document, how to read and control scroll position, how to implement smooth scrolling, and how to prevent scrolling when needed.

Window Width and Height

There are two different questions you might ask about the browser window's size: "How big is the area where the page is displayed?" and "How big is the entire browser window, including toolbars?" JavaScript provides properties for both.

window.innerWidth and window.innerHeight

These properties return the dimensions of the viewport, which is the area inside the browser window where the page content is displayed. This includes the space taken by scrollbars (if visible).

console.log(window.innerWidth);  // e.g., 1440
console.log(window.innerHeight); // e.g., 900

The values update when the user resizes the browser window:

window.addEventListener("resize", () => {
console.log(`Viewport: ${window.innerWidth} x ${window.innerHeight}`);
});

document.documentElement.clientWidth and document.documentElement.clientHeight

These properties also measure the viewport, but they exclude scrollbar width. This is the actual usable area where content can appear.

let docEl = document.documentElement;

console.log(docEl.clientWidth); // e.g., 1423 (1440 minus ~17px scrollbar)
console.log(docEl.clientHeight); // e.g., 900 (no horizontal scrollbar in this case)

The Critical Difference

The distinction between these two approaches matters when the page has a visible vertical scrollbar:

// Page with a vertical scrollbar (content taller than viewport)

console.log(window.innerWidth); // 1440 (includes scrollbar)
console.log(document.documentElement.clientWidth); // 1423 (excludes scrollbar)

// Difference = scrollbar width
let scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
console.log(scrollbarWidth); // ~17 (varies by OS and browser)

When there is no scrollbar, both values are the same:

// Page without a scrollbar (content fits in viewport)

console.log(window.innerWidth); // 1440
console.log(document.documentElement.clientWidth); // 1440
// Same! No scrollbar to account for.

Which One Should You Use?

Use CasePropertyWhy
Positioning elements within the viewportdocument.documentElement.clientWidth/HeightExcludes scrollbar, matches the usable area
CSS media query equivalentdocument.documentElement.clientWidthMatches how CSS @media (max-width: ...) works
Full window size including scrollbarwindow.innerWidth/HeightMatches the total viewport area
Detecting scrollbar presenceCompare bothDifference reveals scrollbar width

For most practical purposes, document.documentElement.clientWidth and clientHeight are the right choice because they give you the dimensions of the area where content can actually be placed:

function getViewportSize() {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
};
}

let viewport = getViewportSize();
console.log(`Usable viewport: ${viewport.width} x ${viewport.height}`);

window.outerWidth and window.outerHeight

These properties return the size of the entire browser window, including toolbars, address bar, and window borders:

console.log(window.outerWidth);  // e.g., 1440
console.log(window.outerHeight); // e.g., 960 (taller than innerHeight due to browser chrome)

console.log(window.outerHeight - window.innerHeight);
// e.g., 60 (height of browser chrome: tabs, address bar, bookmarks bar)

These are rarely used in web development since you typically care about the area available for your content, not the overall browser window size.

Detecting Viewport Size Changes

// Listen for resize events
window.addEventListener("resize", () => {
let width = document.documentElement.clientWidth;
let height = document.documentElement.clientHeight;

console.log(`Viewport resized: ${width} x ${height}`);
});
tip

Resize events fire rapidly during window resizing. If your handler performs expensive operations (DOM updates, calculations), throttle it to avoid performance issues:

let resizeTimeout;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
let width = document.documentElement.clientWidth;
console.log(`Resized to: ${width}px`);
// Perform expensive operations here
}, 150);
});

Document Width and Height

The viewport is what the user sees at any moment. The document is the entire page, including the parts scrolled out of view above, below, and to the sides. The document can be much larger than the viewport.

Getting the Full Document Size

Theoretically, document.documentElement.scrollWidth and scrollHeight should give the full document dimensions. However, due to historical browser inconsistencies, the reliable way to get the full document size is to take the maximum across several properties:

let fullHeight = Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight
);

let fullWidth = Math.max(
document.body.scrollWidth, document.documentElement.scrollWidth,
document.body.offsetWidth, document.documentElement.offsetWidth,
document.body.clientWidth, document.documentElement.clientWidth
);

console.log(`Full document: ${fullWidth} x ${fullHeight}`);

Why Math.max?

Different browsers report the document size through different properties. Some use document.body.scrollHeight, others use document.documentElement.scrollHeight. By taking the maximum of all possible sources, you get the correct value regardless of the browser:

// On a page with 3000px of content:
console.log(document.body.scrollHeight); // 3000 (some browsers)
console.log(document.documentElement.scrollHeight); // 3000 (other browsers)
console.log(document.body.offsetHeight); // may differ
console.log(document.documentElement.offsetHeight); // may differ

// Math.max guarantees the correct answer

Practical Example: Scroll Progress Indicator

Knowing the document height and viewport height lets you calculate how far the user has scrolled through the page:

function getScrollProgress() {
let scrollTop = window.scrollY;
let docHeight = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
);
let viewportHeight = document.documentElement.clientHeight;

// Maximum scrollable distance
let maxScroll = docHeight - viewportHeight;

if (maxScroll <= 0) return 1; // Page fits in viewport, 100% visible

return scrollTop / maxScroll; // 0 to 1
}

window.addEventListener("scroll", () => {
let progress = getScrollProgress();
console.log(`${(progress * 100).toFixed(1)}% scrolled`);
});

Current Scroll Position

To know where the user has scrolled to, JavaScript provides two pairs of properties that give the current scroll offset from the top-left corner of the document.

window.scrollX and window.scrollY

These are the modern, standard properties. They return the number of pixels the document has been scrolled horizontally and vertically:

// At the top of the page:
console.log(window.scrollX); // 0
console.log(window.scrollY); // 0

// After scrolling down 500px:
console.log(window.scrollY); // 500

// After scrolling right 200px:
console.log(window.scrollX); // 200

window.pageXOffset and window.pageYOffset

These are older aliases for scrollX and scrollY. They exist for backward compatibility and return the exact same values:

console.log(window.scrollX === window.pageXOffset); // true
console.log(window.scrollY === window.pageYOffset); // true

Use scrollX/scrollY in new code. You may encounter pageXOffset/pageYOffset in older codebases and tutorials.

document.documentElement.scrollTop and scrollLeft

You can also read the scroll position through document.documentElement:

console.log(document.documentElement.scrollTop);  // Same as scrollY
console.log(document.documentElement.scrollLeft); // Same as scrollX

The difference is that document.documentElement.scrollTop is writable, while window.scrollY is read-only:

// ❌ Cannot set scrollY directly
// window.scrollY = 500; // This does nothing

// ✅ Can set scrollTop on documentElement
document.documentElement.scrollTop = 500; // Scrolls the page

// ✅ Or use scrollTo (preferred, covered next)
window.scrollTo(0, 500);

Tracking Scroll Position

window.addEventListener("scroll", () => {
console.log(`Scroll position: (${scrollX}, ${scrollY})`);
});

A practical example that shows/hides a "back to top" button:

let backToTopBtn = document.getElementById("back-to-top");

window.addEventListener("scroll", () => {
// Show button after scrolling down 300px
backToTopBtn.hidden = window.scrollY < 300;
});

Scrolling Programmatically

JavaScript provides three methods for scrolling the page (or window) to a specific position or element.

window.scrollTo(x, y) or window.scrollTo(options)

Scrolls the document to an absolute position. The coordinates are relative to the top-left corner of the document.

// Scroll to position (0, 0): the very top of the page
window.scrollTo(0, 0);

// Scroll to 500px from the top
window.scrollTo(0, 500);

// Scroll to 200px from the left and 500px from the top
window.scrollTo(200, 500);

You can also pass an options object:

window.scrollTo({
left: 0,
top: 500,
behavior: "smooth" // smooth animation (covered below)
});

window.scrollBy(x, y) or window.scrollBy(options)

Scrolls the document relative to its current position. This adds (or subtracts) from the current scroll position.

// Scroll down 100px from current position
window.scrollBy(0, 100);

// Scroll up 50px from current position
window.scrollBy(0, -50);

// Scroll right 200px
window.scrollBy(200, 0);

// With options object
window.scrollBy({
left: 0,
top: 100,
behavior: "smooth"
});

scrollBy is useful when you want to scroll relative to wherever the user currently is, without needing to know their exact position:

// Scroll down one "page" (viewport height)
function scrollDownOnePage() {
window.scrollBy({
top: document.documentElement.clientHeight,
behavior: "smooth"
});
}

// Scroll up one "page"
function scrollUpOnePage() {
window.scrollBy({
top: -document.documentElement.clientHeight,
behavior: "smooth"
});
}

element.scrollIntoView(options)

This method is called on a specific element and scrolls the page so that the element becomes visible in the viewport. It is the most practical scrolling method for most use cases.

let section = document.getElementById("contact-section");

// Scroll so the element is at the top of the viewport
section.scrollIntoView();

// Same as above (default alignment is "start")
section.scrollIntoView(true);

// Scroll so the element is at the bottom of the viewport
section.scrollIntoView(false);

The options object provides more control:

section.scrollIntoView({
behavior: "smooth", // "smooth" or "instant" (default: "auto")
block: "start", // Vertical alignment: "start", "center", "end", "nearest"
inline: "nearest" // Horizontal alignment: "start", "center", "end", "nearest"
});

The block parameter controls vertical positioning:

ValueBehavior
"start"Element's top edge aligns with the top of the viewport
"center"Element is centered vertically in the viewport
"end"Element's bottom edge aligns with the bottom of the viewport
"nearest"Scrolls the minimum distance needed to make the element visible
// Center the element in the viewport
section.scrollIntoView({ behavior: "smooth", block: "center" });

// Only scroll if the element is not already visible
section.scrollIntoView({ behavior: "smooth", block: "nearest" });

Practical Example: Smooth Scroll Navigation

<nav id="navbar">
<a href="#introduction">Introduction</a>
<a href="#features">Features</a>
<a href="#pricing">Pricing</a>
<a href="#contact">Contact</a>
</nav>

<section id="introduction">...</section>
<section id="features">...</section>
<section id="pricing">...</section>
<section id="contact">...</section>
let navbar = document.getElementById("navbar");

navbar.addEventListener("click", (event) => {
let link = event.target.closest("a[href^='#']");
if (!link) return;

event.preventDefault();

let targetId = link.getAttribute("href").slice(1);
let targetSection = document.getElementById(targetId);

if (targetSection) {
targetSection.scrollIntoView({
behavior: "smooth",
block: "start"
});

// Update URL hash without jumping
history.pushState(null, "", `#${targetId}`);
}
});

Comparison: scrollTo vs. scrollBy vs. scrollIntoView

MethodScrolls ToBest For
scrollTo(x, y)Absolute position in the documentGoing to a known position (e.g., top of page)
scrollBy(x, y)Relative to current positionMoving up/down by a specific amount
element.scrollIntoView()Makes an element visibleNavigating to a specific section or element
// Go to the very top of the page
window.scrollTo(0, 0);

// Scroll down by half the viewport
window.scrollBy(0, window.innerHeight / 2);

// Make a specific element visible
document.getElementById("footer").scrollIntoView({ behavior: "smooth" });

Smooth Scrolling

All three scrolling methods support smooth animated scrolling through the behavior option.

Enabling Smooth Scrolling in JavaScript

// Smooth scroll to top
window.scrollTo({ top: 0, behavior: "smooth" });

// Smooth scroll to an element
document.getElementById("section").scrollIntoView({ behavior: "smooth" });

// Smooth scroll down by 200px
window.scrollBy({ top: 200, behavior: "smooth" });

Enabling Smooth Scrolling via CSS

You can also enable smooth scrolling globally through CSS. This affects all scrolling triggered by anchor links, scrollIntoView, scrollTo, and scrollBy:

html {
scroll-behavior: smooth;
}

With this CSS rule, even regular anchor links (<a href="#section">) will scroll smoothly without any JavaScript:

<!-- With scroll-behavior: smooth on html, this link scrolls smoothly -->
<a href="#contact">Go to Contact</a>

Respecting User Preferences

Some users prefer reduced motion due to vestibular disorders or personal preference. The prefers-reduced-motion media query lets you respect this setting:

/* Smooth scrolling by default */
html {
scroll-behavior: smooth;
}

/* Disable smooth scrolling for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}

In JavaScript, you can check this preference before applying smooth scrolling:

function getScrollBehavior() {
let prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
return prefersReducedMotion ? "instant" : "smooth";
}

// Use it in scroll calls
window.scrollTo({
top: 0,
behavior: getScrollBehavior()
});

document.getElementById("section").scrollIntoView({
behavior: getScrollBehavior(),
block: "start"
});

Smooth Scroll with Offset (Fixed Header)

When you have a fixed header, scrollIntoView scrolls the element behind the header. You need to account for the header height:

function scrollToElement(element, offset = 0) {
let elementPosition = element.getBoundingClientRect().top + window.scrollY;
let targetPosition = elementPosition - offset;

window.scrollTo({
top: targetPosition,
behavior: "smooth"
});
}

// Scroll to an element with a 80px offset for the fixed header
let header = document.querySelector("header");
let headerHeight = header.offsetHeight;

let target = document.getElementById("features");
scrollToElement(target, headerHeight);

Alternatively, CSS scroll-margin-top (or scroll-padding-top on the scroll container) handles this without JavaScript:

/* Add scroll margin to account for fixed header */
section {
scroll-margin-top: 80px; /* Height of your fixed header */
}

Now scrollIntoView automatically accounts for the offset:

document.getElementById("features").scrollIntoView({ behavior: "smooth" });
// Stops 80px above the element, below the fixed header

Preventing Scrolling

Sometimes you need to temporarily prevent the user from scrolling the page. Common use cases include open modals, lightboxes, full-screen menus, or drag operations.

Method 1: CSS overflow: hidden on the Body

The most common and reliable approach is to set overflow: hidden on the <html> or <body> element:

function disableScroll() {
document.body.style.overflow = "hidden";
}

function enableScroll() {
document.body.style.overflow = "";
}

// Usage with a modal
function openModal() {
disableScroll();
document.getElementById("modal").hidden = false;
}

function closeModal() {
enableScroll();
document.getElementById("modal").hidden = true;
}

The Layout Shift Problem

When you set overflow: hidden, the scrollbar disappears. If the page had a scrollbar, removing it causes the content to shift to the right by the scrollbar's width. This creates a jarring visual jump.

The fix is to add padding equal to the scrollbar width when you remove the scrollbar:

function disableScroll() {
// Calculate scrollbar width before hiding it
let scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;

document.body.style.overflow = "hidden";
// Compensate for the missing scrollbar
document.body.style.paddingRight = scrollbarWidth + "px";
}

function enableScroll() {
document.body.style.overflow = "";
document.body.style.paddingRight = "";
}

Preserving Scroll Position

On some mobile browsers and in certain scenarios, setting overflow: hidden on <body> can reset the scroll position. A more robust approach saves and restores the position:

let savedScrollPosition = 0;

function disableScroll() {
savedScrollPosition = window.scrollY;
let scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;

document.body.style.overflow = "hidden";
document.body.style.paddingRight = scrollbarWidth + "px";

// On mobile: fix the body in place
document.body.style.position = "fixed";
document.body.style.top = `-${savedScrollPosition}px`;
document.body.style.width = "100%";
}

function enableScroll() {
document.body.style.overflow = "";
document.body.style.paddingRight = "";
document.body.style.position = "";
document.body.style.top = "";
document.body.style.width = "";

// Restore scroll position
window.scrollTo(0, savedScrollPosition);
}

You can prevent scrolling by intercepting the wheel and touch events:

function preventScroll(event) {
event.preventDefault();
}

function disableScroll() {
// Prevent mouse wheel scrolling
window.addEventListener("wheel", preventScroll, { passive: false });

// Prevent touch scrolling on mobile
window.addEventListener("touchmove", preventScroll, { passive: false });

// Prevent keyboard scrolling (arrows, space, page up/down)
window.addEventListener("keydown", preventScrollKeys);
}

function enableScroll() {
window.removeEventListener("wheel", preventScroll);
window.removeEventListener("touchmove", preventScroll);
window.removeEventListener("keydown", preventScrollKeys);
}

function preventScrollKeys(event) {
let scrollKeys = ["ArrowUp", "ArrowDown", "Space", "PageUp", "PageDown", "Home", "End"];
if (scrollKeys.includes(event.code)) {
event.preventDefault();
}
}
caution

Note the { passive: false } option. Modern browsers treat wheel and touchmove listeners as passive by default (meaning they cannot call preventDefault). You must explicitly set passive: false to be able to prevent scrolling.

// ❌ This will NOT prevent scrolling (passive by default in modern browsers)
window.addEventListener("wheel", (e) => e.preventDefault());

// ✅ This WILL prevent scrolling
window.addEventListener("wheel", (e) => e.preventDefault(), { passive: false });

Method 3: Allowing Scroll Inside a Specific Element

When you have a modal with scrollable content, you want to disable page scrolling while still allowing scrolling inside the modal:

function disablePageScroll() {
let scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
document.body.style.overflow = "hidden";
document.body.style.paddingRight = scrollbarWidth + "px";
}

function enablePageScroll() {
document.body.style.overflow = "";
document.body.style.paddingRight = "";
}

// The modal content has its own overflow: auto
// So it can still scroll independently
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}

.modal-content {
max-height: 80vh;
overflow: auto; /* This element can still scroll */
background: white;
border-radius: 8px;
padding: 24px;
}

Preventing Scroll in a Specific Direction Only

// Prevent horizontal scrolling only
function disableHorizontalScroll() {
window.addEventListener("scroll", () => {
if (window.scrollX !== 0) {
window.scrollTo(0, window.scrollY);
}
});
}

// Prevent vertical scrolling beyond a certain point
function limitVerticalScroll(maxY) {
window.addEventListener("scroll", () => {
if (window.scrollY > maxY) {
window.scrollTo(window.scrollX, maxY);
}
});
}

Complete Practical Example

Here is a comprehensive example that combines window measurements, scroll tracking, smooth navigation, and scroll prevention in a realistic page layout:

<!DOCTYPE html>
<html>
<head>
<title>Window Scrolling Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }

body { font-family: system-ui, sans-serif; }

header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
display: flex;
align-items: center;
padding: 0 20px;
z-index: 100;
transition: transform 0.3s;
}

header.header-hidden { transform: translateY(-100%); }

.progress-bar {
position: fixed;
top: 60px;
left: 0;
height: 3px;
background: #2196f3;
z-index: 101;
transition: width 0.1s;
}

nav a {
margin: 0 12px;
text-decoration: none;
color: #333;
}

section {
min-height: 100vh;
padding: 100px 40px 60px;
scroll-margin-top: 64px;
}

section:nth-child(odd) { background: #f5f5f5; }

.back-to-top {
position: fixed;
bottom: 30px;
right: 30px;
width: 48px;
height: 48px;
border-radius: 50%;
background: #2196f3;
color: white;
border: none;
font-size: 24px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
transition: opacity 0.3s, transform 0.3s;
}

.back-to-top[hidden] { display: block; opacity: 0; pointer-events: none; transform: translateY(20px); }
.back-to-top:not([hidden]) { opacity: 1; transform: translateY(0); }

.scroll-info {
position: fixed;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.8);
color: #0f0;
font-family: monospace;
font-size: 12px;
padding: 10px;
border-radius: 4px;
z-index: 200;
line-height: 1.6;
}

.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 300;
}

.modal-content {
background: white;
padding: 32px;
border-radius: 12px;
max-width: 400px;
text-align: center;
}
</style>
</head>
<body>
<header id="header">
<nav>
<a href="#hero">Home</a>
<a href="#features">Features</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
<a href="#" id="modal-trigger">Modal</a>
</nav>
</header>

<div class="progress-bar" id="progress-bar"></div>

<section id="hero"><h1>Welcome</h1><p>Scroll down to explore.</p></section>
<section id="features"><h1>Features</h1><p>Feature content here.</p></section>
<section id="about"><h1>About</h1><p>About content here.</p></section>
<section id="contact"><h1>Contact</h1><p>Contact content here.</p></section>

<button class="back-to-top" id="back-to-top" hidden></button>

<div class="scroll-info" id="scroll-info"></div>

<div class="modal-overlay" id="modal" hidden>
<div class="modal-content">
<h2>Modal Window</h2>
<p>Page scrolling is disabled while this modal is open.</p>
<button id="close-modal">Close</button>
</div>
</div>

<script>
let header = document.getElementById("header");
let progressBar = document.getElementById("progress-bar");
let backToTop = document.getElementById("back-to-top");
let scrollInfo = document.getElementById("scroll-info");
let modal = document.getElementById("modal");

// --- Smooth Navigation ---
document.querySelector("nav").addEventListener("click", (event) => {
let link = event.target.closest("a[href^='#']");
if (!link || link.id === "modal-trigger") return;
event.preventDefault();

let targetId = link.getAttribute("href").slice(1);
let target = document.getElementById(targetId);
if (target) {
target.scrollIntoView({ behavior: "smooth", block: "start" });
}
});

// --- Scroll Tracking ---
let lastScrollY = 0;

window.addEventListener("scroll", () => {
let scrollY = window.scrollY;
let docHeight = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
);
let viewportHeight = document.documentElement.clientHeight;
let maxScroll = docHeight - viewportHeight;
let progress = maxScroll > 0 ? scrollY / maxScroll : 0;

// Update progress bar
progressBar.style.width = `${progress * 100}%`;

// Show/hide back-to-top button
backToTop.hidden = scrollY < 300;

// Auto-hide header on scroll down, show on scroll up
if (scrollY > lastScrollY && scrollY > 200) {
header.classList.add("header-hidden");
} else {
header.classList.remove("header-hidden");
}
lastScrollY = scrollY;

// Update debug info
scrollInfo.innerHTML = `
scrollY: ${scrollY.toFixed(0)}px<br>
viewport: ${document.documentElement.clientWidth} x ${viewportHeight}<br>
document: ${docHeight}px tall<br>
progress: ${(progress * 100).toFixed(1)}%<br>
scrollbar: ${window.innerWidth - document.documentElement.clientWidth}px
`;
});

// --- Back to Top ---
backToTop.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});

// --- Modal with Scroll Lock ---
let savedScrollPosition = 0;

document.getElementById("modal-trigger").addEventListener("click", (event) => {
event.preventDefault();

savedScrollPosition = window.scrollY;
let scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;

document.body.style.overflow = "hidden";
document.body.style.paddingRight = scrollbarWidth + "px";
header.style.paddingRight = scrollbarWidth + "px";

modal.hidden = false;
});

document.getElementById("close-modal").addEventListener("click", () => {
document.body.style.overflow = "";
document.body.style.paddingRight = "";
header.style.paddingRight = "";

modal.hidden = true;
window.scrollTo(0, savedScrollPosition);
});

// Close modal on overlay click (not on content click)
modal.addEventListener("click", (event) => {
if (event.target === modal) {
document.getElementById("close-modal").click();
}
});

// Trigger initial scroll event to populate info
window.dispatchEvent(new Event("scroll"));
</script>
</body>
</html>

This example demonstrates:

  • Viewport measurement with document.documentElement.clientWidth/clientHeight
  • Document size calculation with Math.max across multiple sources
  • Scroll position tracking with window.scrollY
  • Scroll progress calculation for the progress bar
  • Smooth navigation with scrollIntoView and scroll-margin-top
  • Programmatic scrolling with scrollTo for the back-to-top button
  • Auto-hiding header based on scroll direction
  • Scroll prevention with overflow: hidden and scrollbar width compensation for the modal

Summary

JavaScript provides a complete set of properties and methods for measuring the window and controlling scroll behavior.

Window/Viewport Size:

PropertyWhat It Measures
window.innerWidth/HeightViewport including scrollbar
document.documentElement.clientWidth/HeightViewport excluding scrollbar (usable area)
window.outerWidth/HeightEntire browser window including chrome

Use document.documentElement.clientWidth/Height for the usable viewport area. Use window.innerWidth when you need to include the scrollbar (e.g., calculating scrollbar width).

Document Size:

  • Use Math.max across scrollHeight, offsetHeight, and clientHeight on both document.body and document.documentElement for reliable cross-browser results.

Current Scroll Position:

PropertyReadWrite
window.scrollX / window.scrollYYesNo
window.pageXOffset / window.pageYOffsetYes (legacy alias)No
document.documentElement.scrollLeft/TopYesYes

Programmatic Scrolling:

MethodBehavior
scrollTo(x, y) or scrollTo(options)Scroll to absolute position
scrollBy(x, y) or scrollBy(options)Scroll relative to current position
element.scrollIntoView(options)Scroll until element is visible

All three accept { behavior: "smooth" } for animated scrolling.

Smooth Scrolling:

  • JavaScript: pass { behavior: "smooth" } to scroll methods.
  • CSS: set scroll-behavior: smooth on html for global smooth scrolling.
  • Use scroll-margin-top on target elements to account for fixed headers.
  • Respect prefers-reduced-motion for accessibility.

Preventing Scrolling:

  • Set document.body.style.overflow = "hidden" (most reliable).
  • Compensate for scrollbar disappearance with paddingRight.
  • On mobile, also set position: fixed and save/restore scroll position.
  • For event-based prevention, use { passive: false } on wheel and touchmove listeners.