Features Full Page Screenshot Pricing Docs Blog Log In Sign Up

Full Page Screenshot API

The top of the page looks fine. Everything below the fold is gray boxes, missing images, or content that never rendered. That's what a naive full-page capture produces on most modern sites, because lazy loading, sticky headers, and client-side hydration all break the assumption that "the page is ready" the moment the HTML arrives.

full_page handles the scroll-and-render cycle. One parameter covers about 90% of pages. The remaining 10% need one more parameter each, and the sections below cover every failure mode I've seen in production.

How full_page works

Pass "full_page": true and the renderer measures document.documentElement.scrollHeight (the entire scrollable area), then sets the viewport to match before capturing. Width stays at whatever you specify, 1280px by default. Height extends to the real content boundary.

curl -X POST https://screenshotrun.com/api/v1/screenshots \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "full_page": true
  }'

One continuous image. No cropping, no stitching. The response includes actual dimensions ("width": 1280, "height": 4850 or whatever the page measures), so you know exactly what you're working with before downloading.

For static pages, that's all you need. Dynamic ones are messier.

Lazy-loaded images render as blank rectangles

Try capturing a product listing on shopify.com or any content-heavy site built after 2019. Scroll down in the resulting image and you'll see gray or transparent boxes where product photos should be. The DOM structure is there. The image data never loaded.

Full-page screenshot showing gray placeholder boxes where product images should appear below the fold
Without delay, lazy-loaded images render as blank gray boxes below the fold

Here's why: most sites use intersection observers to defer images until scroll reveals them. A headless browser capturing the full page sees the <img> tags but never triggers the observers, because no human scrolled. The renderer does scroll the document during full-page capture, which fires most observers, but those images still need time to actually download.

Adding delay gives them that time:

{
  "url": "https://example.com/products",
  "full_page": true,
  "delay": 3
}

Three seconds handles most pages. For sites with custom lazy loaders that ignore scroll position entirely, wait_for_selector holds the capture until a specific element appears. Waiting for footer works as a reliable heuristic, since it means everything above has rendered. More patterns in the lazy loading deep-dive.

SPAs show an empty shell before hydration

React, Vue, and Next.js apps start with a near-empty <div id="root"></div>. Content arrives after JavaScript hydrates the framework. Capture too early and you get the loading spinner, not the dashboard.

Screenshot of an SPA showing only a loading spinner and empty layout before hydration completes
Capturing an SPA too early produces a loading spinner instead of the actual dashboard

Wait for an element that only exists after render:

{
  "url": "https://myapp.example.com/dashboard",
  "full_page": true,
  "wait_for_selector": "[data-testid='dashboard-content']",
  "delay": 2
}

Pick a selector tied to the rendered component, not the skeleton. A data-testid attribute works well. So does a content heading, a product grid, or a nav item that appears after auth resolves. SSR frameworks like Next.js and Nuxt are easier since the HTML arrives pre-rendered. More SPA-specific patterns in the SPA screenshot guide.

Sticky headers repeat down the entire image

Open any site with a position: fixed nav bar. Now imagine capturing that page as one 5000px-tall image. The header stays pinned to the viewport during scroll, so it appears at the top and overlaps content further down, sometimes repeating like a watermark across the entire height.

Full-page screenshot showing a fixed navigation bar overlapping content at multiple scroll positions
A position: fixed header overlaps content when captured as one tall image

Two approaches. Remove it entirely with hide_selectors:

{
  "url": "https://example.com",
  "full_page": true,
  "hide_selectors": ["header.sticky", "nav.fixed-top", ".floating-cta"]
}

Or keep it visible once at the top by overriding the positioning via css:

{
  "url": "https://example.com",
  "full_page": true,
  "css": "header, nav { position: relative !important; top: auto !important; }"
}

I chose to handle this at the CSS level rather than stripping DOM nodes because some pages rely on header height for layout calculations. Remove the element and everything below shifts up by 60-80px.

Cookie banners block the hero section

block_cookies defaults to true, which strips most consent managers (OneTrust, CookieBot, Didomi, and others) automatically. For custom-built consent dialogs, combine click_selector with hide_selectors:

{
  "url": "https://example.com",
  "full_page": true,
  "click_selector": "#consent-accept-all",
  "hide_selectors": [".cookie-overlay", ".gdpr-backdrop"]
}

click_selector clicks the element before capture. Useful beyond cookies: dismissing newsletter popups, expanding collapsed sections, closing GDPR overlays. More on the mechanics in the cookie banner blocking post.

Retina doubles dimensions, quadruples file size

A 1280 x 5000 full-page PNG weighs 3-5 MB. Turn on retina and that becomes 2560 x 10000 at 15-25 MB. Complex pages hit 50 MB. The right format depends on what you're building:

Use caseRecommended setupTypical size
Visual regression testingretina + webp quality 9060-70% of PNG
Directory thumbnailswebp quality 75 + resize_width 800Under 200 KB
Archiving / complianceretina + avif quality 8040-50% smaller than WebP

AVIF produces the smallest files at equivalent perceptual quality. The tradeoff: encoding takes longer on the API side, so expect slightly higher response times for AVIF compared to WebP.

Infinite scroll pages have no "full page"

Twitter, Pinterest, Reddit. These pages load content endlessly. The API captures what's in the DOM at the moment of capture, up to the timeout (default 30s, max 60s). It won't simulate scrolling to trigger new loads, and I haven't found a reliable way to make that work generically.

For infinite scroll feeds, set a fixed viewport height instead of using full_page:

{
  "url": "https://example.com/feed",
  "width": 1280,
  "height": 3000,
  "delay": 3
}

Three to four screenfuls. Predictable image height. Much easier to layout in a UI than a variable-height capture.

Blank screenshots: three common causes

A completely white image (not partial, completely blank) usually comes from one of three things. Detailed troubleshooting in the blank screenshots guide.

Bot detection

The site blocks headless browsers before the page loads. Fix: "stealth": true.

Viewport-dependent layout

Some SPAs calculate layout from window.innerHeight. When full_page changes the effective viewport, layout breaks. Fix: set an explicit height alongside full_page.

Scrollable container instead of body

If the page uses overflow: auto on a wrapper div, full_page measures the body (viewport-height), not the inner scrollable area. Fix: target the container with selector or inject JS to restructure the DOM. There's no universal fix for this one because the page structure varies too much across sites.

Caching avoids re-rendering the same page

Full-page captures run slower than viewport shots. Expect 8-10 seconds on complex pages with heavy DOM trees. If you're capturing the same URL repeatedly, cache_ttl returns a cached result instead of re-rendering:

{
  "url": "https://example.com",
  "full_page": true,
  "cache_ttl": 3600
}

Any parameter change (including delay or width) produces a different cache key. Keep parameters consistent for cache hits. More on cache mechanics in the caching post.

Parameters that pair with full_page

Every parameter in the API reference works with full_page. These are the ones that matter most for full-page captures specifically:

ParameterEffect with full_pageTypical use
delayWaits N seconds after scrollLazy-loaded images
wait_for_selectorHolds capture until element existsSPAs, async content
hide_selectorsHides up to 20 elementsSticky headers, floating CTAs
click_selectorClicks before captureDismiss modals, accept consent
cssInjects custom CSSOverride positioning, reset animations
jsRuns JavaScriptAdvance carousels, trigger loaders
block_cookiesBlocks consent (default: true)Clean hero section
retina2x resolutionPrint, high-DPI
formatPNG/JPEG/WebP/AVIF/TIFF/PDFWebP/AVIF for compression
resize_widthDownsizes after captureThumbnails
reduced_motionDisables CSS animationsScroll-triggered animations
cache_ttlReturns cached resultRepeated captures
stealthBypasses bot detectionProtected sites

full_page is available on every plan, including the free tier (200 screenshots/month). No restrictions on page height or file size.

Start with full_page + delay + block_cookies. That combination handles about 90% of pages cleanly. When something breaks, the fix is usually one more parameter: hide_selectors for sticky elements, wait_for_selector for SPAs, or css for scroll-triggered animations. For deeper dives into specific failure modes, see the visual regression testing and website directories use cases.

Capture full pages with one parameter

Get your API key — 200 free/month

Frequently asked questions

The renderer scrolls the full document height during capture, which triggers most intersection observer-based lazy loaders. Adding a `delay` of 2-3 seconds gives those images time to load after the scroll. For pages with custom lazy loading that doesn't respond to scroll position, use `wait_for_selector` to hold the capture until a specific element (like the footer) has rendered.
Fixed or sticky-positioned elements stay pinned to the viewport during the scroll-and-capture process, causing them to appear multiple times in the output. Use `hide_selectors` to remove them before capture, or inject CSS via the `css` parameter to change their position to `relative`, which keeps them visible once at their natural document position.
A full-page screenshot at 2x retina resolution can reach 15-50 MB in PNG format. Switch to WebP (quality 90 for near-lossless) or AVIF (quality 80 for best compression) to cut file size by 50-70%. If you don't need full resolution, use `resize_width` to downscale the output server-side after capture.
Yes, but SPAs often need `wait_for_selector` to ensure the JavaScript framework has finished rendering before capture. Target a selector that only exists after hydration — a data-testid attribute, a content heading, or a component that loads after the app mounts. SSR frameworks like Next.js and Nuxt are easier since the HTML arrives pre-rendered.
Yes. `full_page` is available on every plan, including the free tier (200 screenshots per month). There are no restrictions on page height, file size, or which parameters you can combine with it.