How to Capture a Full-Page Screenshot with Selenium When the Page Has Sticky Navigation
Capture the whole document with Selenium, handle sticky navigation without hiding content, and troubleshoot Firefox and Chromium differences.
To capture a full document, use a browser-specific full-page mechanism: Selenium’s Firefox driver provides full-document screenshot methods; Chromium can use Chrome DevTools Protocol (CDP) with Page.captureScreenshot and captureBeyondViewport: true. The ordinary WebDriver screenshot captures the current browsing context, so do not assume it includes content below the viewport. Sticky navigation is a CSS layout behavior, not a Selenium option. If the header repeats, obscures content, or appears in the wrong place, temporarily neutralize its sticky positioning for the capture, then restore the original styles.
Choose first whether the screenshot should show the navigation once at the top, keep its normal sticky behavior, or omit it. The examples below use Python and Selenium. They wait for the page to load, optionally load lazy content, apply a reversible CSS override, and save a PNG.
1. Choose the capture method
| Browser | Full-page approach | Notes |
|---|---|---|
| Firefox | get_full_page_screenshot_as_file() |
Documented Selenium Firefox API; writes PNG. |
| Chromium | CDP Page.captureScreenshot with captureBeyondViewport: true |
Browser-specific protocol. Pin and verify browser/driver versions. |
| Any WebDriver browser | save_screenshot() |
Convenient viewport screenshot; not a full-document guarantee. |
Firefox’s API also returns PNG bytes or base64. The CDP protocol accepts format, quality for JPEG, an optional clip region, and the beyond-viewport flag. That flag defaults to false in the cited protocol definition, so set it explicitly. See the Selenium Firefox API and the CDP Page domain.
2. Identify the sticky element and its scroll context
A sticky element is positioned relative to its nearest ancestor with a scrolling mechanism and its containing block. An ancestor with overflow: hidden, auto, scroll, or overlay may affect which container controls the sticky behavior. Inspect the page in browser developer tools, then select the actual header or navigation element, such as header.site-header. Avoid broad selectors like header if the page has multiple headers.
CSS semantics explain why changing position can help, but screenshot behavior can differ by browser, page structure, and capture path. Treat the override as a workaround to validate against the page and browser you automate. See MDN’s position documentation.
3. Runnable Python example for Firefox
Install Selenium with python -m pip install selenium. Selenium Manager can obtain a compatible driver in supported setups; environments with restricted network access can configure the browser and driver separately. Set the URL and sticky selector for the target page.
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait
URL = "https://example.com/long-page"
STICKY_SELECTOR = "header.site-header" # Replace with the site's selector
OUTPUT = Path("full-page.png")
options = Options()
options.add_argument("-headless")
driver = webdriver.Firefox(options=options)
try:
driver.set_window_size(1440, 1000)
driver.get(URL)
WebDriverWait(driver, 30).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
# Optional: scroll through the document to trigger common lazy-loading
# behavior. This is a practical heuristic, not a guarantee for every site.
driver.execute_script("""
const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
const maxY = document.documentElement.scrollHeight;
let y = 0;
const timer = setInterval(() => {
window.scrollTo(0, y);
y += step;
if (y >= maxY) {
clearInterval(timer);
window.scrollTo(0, 0);
window.__scrollPassDone = true;
}
}, 80);
""")
WebDriverWait(driver, 20).until(
lambda d: d.execute_script("return window.__scrollPassDone === true")
)
# Remove sticky/fixed positioning for this capture only. The style element
# is removed in finally so the live page is restored even if saving fails.
driver.execute_script("""
const style = document.createElement('style');
style.id = '__screenshot_override';
style.textContent = arguments[0] +
' { position: static !important; inset: auto !important; }';
document.head.appendChild(style);
""", STICKY_SELECTOR)
# Allow a frame for layout and paint after the CSS override.
driver.execute_async_script("""
const done = arguments[arguments.length - 1];
requestAnimationFrame(() => requestAnimationFrame(done));
""")
if not driver.get_full_page_screenshot_as_file(str(OUTPUT.resolve())):
raise OSError(f"Could not write screenshot to {OUTPUT.resolve()}")
print(f"Saved {OUTPUT.resolve()}")
finally:
driver.execute_script("""
document.getElementById('__screenshot_override')?.remove();
""")
driver.quit()
The scroll pass is optional. It can trigger images or sections that load near the viewport, but pages may use different lazy-loading triggers. If the page changes content as it scrolls, wait for the site’s own readiness condition or use a narrower, site-specific scroll strategy. The document.readyState check means the initial document load completed; it does not prove that every asynchronous widget or image has settled.
Firefox variations
To keep the sticky behavior, omit the CSS override. To get bytes instead of writing a file through the driver, replace the save call with:
png_bytes = driver.get_full_page_screenshot_as_png()
OUTPUT.write_bytes(png_bytes)
To capture the current viewport for debugging, use driver.save_screenshot("viewport.png"). That is not a substitute for the full-document method.
4. Runnable Python example for Chromium using CDP
Use Selenium’s Chromium driver and issue the CDP command directly. The example measures document dimensions, removes sticky positioning temporarily, requests a beyond-viewport PNG, and decodes the returned base64 data.
import base64
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
URL = "https://example.com/long-page"
STICKY_SELECTOR = "header.site-header" # Replace with the site's selector
OUTPUT = Path("full-page-chromium.png")
options = Options()
options.add_argument("--headless=new")
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)
try:
driver.get(URL)
WebDriverWait(driver, 30).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
driver.execute_script("""
const style = document.createElement('style');
style.id = '__screenshot_override';
style.textContent = arguments[0] +
' { position: static !important; inset: auto !important; }';
document.head.appendChild(style);
""", STICKY_SELECTOR)
driver.execute_async_script("""
const done = arguments[arguments.length - 1];
requestAnimationFrame(() => requestAnimationFrame(done));
""")
size = driver.execute_script("""
const e = document.documentElement;
const b = document.body;
return {
width: Math.max(e.scrollWidth, e.clientWidth, b ? b.scrollWidth : 0),
height: Math.max(e.scrollHeight, e.clientHeight, b ? b.scrollHeight : 0)
};
""")
result = driver.execute_cdp_cmd("Page.captureScreenshot", {
"format": "png",
"captureBeyondViewport": True,
"clip": {
"x": 0,
"y": 0,
"width": size["width"],
"height": size["height"],
"scale": 1
}
})
OUTPUT.write_bytes(base64.b64decode(result["data"]))
print(f"Saved {OUTPUT.resolve()} ({size['width']} × {size['height']} CSS pixels)")
finally:
driver.execute_script("""
document.getElementById('__screenshot_override')?.remove();
""")
driver.quit()
The clip makes the requested document rectangle explicit. If capture fails on an unusually tall or wide page, try removing the clip argument while keeping captureBeyondViewport enabled, or capture sections and stitch them using your own image-processing pipeline. Very large images consume substantial memory and can exceed browser or image-library limits. CDP is Chromium-specific and its protocol can change; check your Selenium, Chrome, and driver versions when upgrading.
5. Decide what to do with sticky navigation
| Desired result | Approach | Watch for |
|---|---|---|
| Navigation appears once at the document top | Temporarily set the sticky element to position: static. |
Static positioning may alter layout if the original header used offsets or scripts tied to its position. |
| Navigation keeps its visual style but does not stick | Override position and sticky inset values; consider preserving dimensions and background. |
Other CSS such as transforms, z-index, or scroll-container clipping may still affect the result. |
| Navigation is absent | Hide the specific element for capture with display: none. |
Hiding it can change the document flow and shift content. |
| Sticky effect should remain as seen in normal browsing | Do not modify the style; capture and inspect the output. | Full-page capture does not represent one real viewport moment, so sticky behavior may look unexpected. |
Prefer injecting a temporary style over permanently changing the site. The examples remove the injected style in a finally block. If your test reuses a driver and needs the original page state afterward, also return scroll position to the top and remove any test-created DOM changes.
6. Wait for content and handle lazy loading
- Wait for a meaningful selector or application-ready signal in addition to document readiness. For example, wait for the main article, a loading indicator to disappear, or a test-specific JavaScript flag.
- If below-the-fold images are lazy-loaded, scroll through the page in increments and wait for image completion where appropriate. Some sites load only after intersection, user interaction, or a delay.
- Return to the top before capture if the capture path or application state depends on scroll position.
- Freeze animations or transitions only if visual consistency requires it. Such overrides can affect the page and should be scoped to capture.
- For pages with infinite scroll, set a deliberate maximum scroll depth or item count. An unbounded scroll loop can run forever or capture an unstable page.
A generic image readiness check can help diagnose missing images:
WebDriverWait(driver, 20).until(lambda d: d.execute_script("""
return Array.from(document.images).every(img => img.complete);
"""))
This condition only checks the document’s current image elements. It does not guarantee successful decoding, successful network responses, or that further images will not be added later. For stronger checks, inspect naturalWidth, wait for page-specific content, and use a bounded timeout.
7. cURL, Python, and Node.js options with ScreenshotNeo
If you do not need Selenium’s browser session or its exact local environment, ScreenshotNeo provides a screenshot API and MCP server. Its API supports full-page capture and selector-based element capture. For a full-page capture, use one request:
ScreenshotNeo API documentation
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 accepts and removes cookie or consent banners from 60+ known platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the page verdict and billing status in headers. Its MCP server includes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Free includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000, and every feature is on every plan.
Sign up for 1,000 free screenshots a month, no card required.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot stops at the viewport | Using ordinary WebDriver screenshot or CDP’s default beyond-viewport setting. | Use Firefox’s full-page method or set CDP captureBeyondViewport to true. |
| Sticky nav covers text or appears oddly | The capture’s layout treatment differs from a normal scroll, or a scroll ancestor/containing block affects the element. | Inspect the sticky element and ancestors; test a temporary scoped positioning override and review the output. |
| Header selector finds nothing | Selector is wrong, element is in an iframe or shadow root, or page content is not ready. | Wait for the element, verify the selector in developer tools, and switch into the relevant frame or handle the shadow root. |
| Lower images are blank | Lazy loading has not triggered or image requests have not completed. | Use a bounded scroll pass, wait for relevant images or page-specific readiness, and check failed requests. |
| Screenshot file is missing | Relative path points somewhere unexpected, output directory does not exist, or Firefox returned false. | Use an absolute path, create the directory, and check the method result or write the returned bytes yourself. |
| CDP command is unknown or errors | Browser is not Chromium, or browser and protocol assumptions differ. | Use the matching Chromium driver and verify versions; use Firefox’s API for Firefox. |
| Capture is clipped, blank, or too large | Incorrect document dimensions, unusual page layout, browser limits, or memory pressure. | Inspect measured dimensions, retry without an explicit clip, reduce viewport/device scale where applicable, or split the page into bounded sections. |
| Content differs between runs | Dynamic data, animation, personalization, late scripts, or network timing. | Wait on page-specific conditions, stabilize test data, disable animations for capture if appropriate, and set a clear timeout policy. |
| Override breaks header or page layout | The site depends on sticky positioning, inset values, or scripts reacting to scroll state. | Use a narrower selector, preserve needed dimensions, capture without override, or hide only the header if that matches the desired image. |
9. Performance, reliability, and cost considerations
- Image size: A full-page image’s pixel area grows with document width and height. Larger images take longer to encode, transfer, and store and use more memory. Use PNG for lossless output; JPEG can reduce size for photographic content, with a quality tradeoff.
- Capture stability: Fix the viewport, browser version, device scale, and page data when comparing images. Wait for the same readiness condition each run. Record browser and driver versions alongside failures.
- Retries: Retry transient navigation or browser startup failures with a bounded count and fresh state. Do not repeatedly retry deterministic selector, authentication, or protocol errors without changing the cause.
- Timeouts: Use explicit page-load and readiness timeouts. Treat timeout as a failed capture and preserve logs or the viewport screenshot for diagnosis.
- Cost: Selenium’s direct resource costs depend on where and how the browser runs; this guide makes no fixed hosting-cost or speed claim. ScreenshotNeo’s stated plans are Free 1,000 per month, Starter $5/3,000, Growth $15/15,000, Pro $39/60,000, Scale $99/250,000, and Business $249/1,000,000; yearly billing gives two months free. Every feature is on every plan.
10. FAQ
Does Selenium have one cross-browser full-page screenshot command?
No. Use the browser-specific mechanism: Firefox’s documented full-document API or Chromium’s CDP capture.
Should I remove sticky positioning every time?
No. Remove it only when the desired image is wrong with the default layout. Inspect the result because sticky behavior and full-page capture interact with page structure.
Can I use this for a page that requires login?
Yes, if your test has an authorized authenticated browser session. Navigate and establish the session before capture, and avoid putting credentials in source code or logs.
Is a full-page screenshot equivalent to stitching viewport screenshots?
No. Browser full-page capture uses browser capture behavior; manual scrolling and stitching can introduce seams, repeated sticky elements, or content shifts.


