# Why website screenshots look wrong, and the fixes

> A field guide to 11 ways a website screenshot goes wrong, from blank lazy images to cookie banners and cut-off pages, with the cause and the fix for each.

Field guide by Serghei Pogor, founder of CurlShot (https://curlshot.com/about/serghei-pogor). Published 14 October 2026. Page for people: https://curlshot.com/blog/why-website-screenshots-look-wrong

A website screenshot looks wrong for one of 11 reasons, and most of them come down to timing or state: the capture was taken before lazy images, script-fetched data, web fonts or animations had finished, or the browser arrived as a first-time, signed-out, desktop visitor. The browser's load event does not wait for lazy images, and "network idle" only means 500 ms without network connections. The fixes are to scroll the page first, wait for a named element instead of a fixed time, hide fixed overlays, and set the viewport and pixel density on purpose.

## Key facts

- The load event fires without waiting for images marked loading="lazy", so a capture taken at load can show empty image boxes further down the page.
- Playwright and Puppeteer define network idle as no network connections for at least 500 ms. A page that polls a server never reaches it.
- Cookie banners and sticky headers use position: fixed or sticky, which ties them to the viewport: they cover content, and scroll-and-stitch tools repeat them.
- Image width = viewport width x device scale factor: 1280 x 2 = 2560 pixels. A scale 1 capture looks soft on a HiDPI screen.
- Without mobile emulation, a phone-sized capture can lay the page out about 980 pixels wide and shrink it to fit.
- In CurlShot the matching options are full_page (it scrolls first), wait_for_selector, block_cookie_banners, hide_selectors, reduced_motion, viewport_device and device_scale_factor.

## Diagnosis table: what is wrong with my screenshot?

Find the symptom in the first column. Numbers in square brackets refer to the sources at the end.

| If you see | It is | First thing to try |
| --- | --- | --- |
| Empty or grey boxes where images should be, mostly further down the page | 1. Blank image boxes | Scroll through the page before the capture |
| A spinner, grey skeleton blocks, an empty chart or a "Loading" label | 2. Spinners and skeletons | Wait for an element of the real content, not for a fixed time |
| A consent box over the bottom of the page, or a dimmed overlay over all of it | 3. Cookie banner on top | Block or hide the banner, or set the consent cookie first |
| The navigation bar again and again down a long screenshot, or a bar across the middle | 4. Repeated sticky header | Hide the bar or make it static before a full-page capture |
| The screenshot stops at the first screen, or ends partway down | 5. Cut-off page | Turn full-page capture on and check the height limit |
| Text in a default serif or system font, different line breaks, or missing text | 6. Fallback fonts | Wait for the fonts to finish loading |
| Sections half transparent or shifted, or two captures of one page that differ | 7. Frozen mid-animation | Disable animations, or ask the page for reduced motion |
| A phone-sized screenshot of the desktop site in tiny text, or the mobile menu on desktop | 8. Wrong layout for the device | Set width, height and mobile emulation together |
| Soft or pixelated text and icons, most of all on a laptop or phone screen | 9. Blurry text | Capture at a device scale factor of 2 |
| A sign-in form in place of the page you asked for | 10. Login wall | Send a session cookie or a token with the request |
| A "checking your browser" page, a CAPTCHA or an "Access denied" message | 11. Bot-check page | Allow your capture tool on your own site; ask the owner on anyone else's |

## The 11 symptoms, with cause and fix

### 1. Blank image boxes

- **What you see:** The top of the page is fine. Further down, images are empty boxes, grey placeholders or blurred thumbnails.
- **Why it happens:** An image marked `loading="lazy"` is not fetched until it reaches a distance from the viewport that the browser calculates, and the `load` event does not wait for it. Scripts built on `IntersectionObserver` hold back images and whole sections the same way. A capture that never scrolls never triggers them. [1][7][3]
- **Fix it by hand:** Scroll slowly to the bottom, wait for the last images, scroll back to the top, then capture.
- **Fix it in an automated capture:** Scroll the page in steps of about one viewport before the capture, pause at each step, then check that every `img` reports `complete`.
- **In CurlShot:** `full_page=true` scrolls through the page first. That step is `full_page_scroll`, on by default. The scroll has a time budget, so on a very long page lower `full_page_max_height`. See [Full-page screenshots](https://curlshot.com/docs/full-page).

### 2. Spinners and skeletons

- **What you see:** A spinner, grey skeleton blocks, an empty chart or a "Loading" label sits where the content should be.
- **Why it happens:** The `load` event covers the page and the files it lists, not the data its scripts request afterwards. An app that renders in the browser fetches that data after `load`, so a capture at `load` shows its loading state. Network idle, which Playwright and Puppeteer define as no network connections for at least 500 ms, comes later, and Playwright marks it as discouraged for tests in favour of checking for the content itself. [7][8][9]
- **Fix it by hand:** Wait until the content is on screen, then capture.
- **Fix it in an automated capture:** Wait for a selector that exists only once the real content is drawn, such as a table row or the chart itself. Use network idle when you cannot name one, and a fixed delay as the last resort.
- **In CurlShot:** `wait_for_selector` waits until the element is visible. `wait_until=networkidle` is the fallback, and `delay` adds up to 30 seconds after both. See [Waiting and timing](https://curlshot.com/docs/waiting).

### 3. Cookie banner on top

- **What you see:** A consent box covers the bottom third of the screenshot, or a dimmed overlay sits over the whole page.
- **Why it happens:** A consent banner is usually a `position: fixed` element, which is placed relative to the viewport and not the page, so it sits on top of whatever is on screen. An automated browser starts with no cookies, so the site treats every capture as a first visit and shows the banner every time. [4]
- **Fix it by hand:** Answer the banner, wait for it to close, then capture.
- **Fix it in an automated capture:** Pick one: set the site's consent cookie before loading, click the banner's button, block the consent tool's script, or hide the banner with a CSS rule.
- **In CurlShot:** `block_cookie_banners=true` blocks known consent scripts, hides their layouts and presses a button on banners it does not recognise. It is best effort: for a home-made banner add `hide_selectors`, `click` or `cookies`. See [Screenshot without cookie banners](https://curlshot.com/tools/screenshot-without-cookie-banners).

### 4. Repeated sticky header

- **What you see:** The navigation bar appears again and again down a long screenshot, or one floating bar covers a line of text in the middle of it.
- **Why it happens:** A `position: fixed` element is placed relative to the viewport, and a `position: sticky` element stays pinned inside its scrolling ancestor. A tool that scrolls, captures each screen and stitches the pieces records the bar once per piece. A browser that renders the whole page in one pass draws it once, though a floating bar can still land where it covers content. [4]
- **Fix it by hand:** Try the browser's own full-page capture: in Chrome DevTools device mode, open More options and choose Capture full size screenshot. If a bar still gets in the way, delete it in the Elements panel first.
- **Fix it in an automated capture:** Before the capture, hide the element or switch it to `position: static` with an injected style rule, so it scrolls away with the page.
- **In CurlShot:** `hide_selectors=.site-header` removes the bar. `styles` takes your own CSS if you would rather un-stick it than hide it. See [Sticky headers in full-page screenshots](https://curlshot.com/docs/full-page).

### 5. Cut-off page

- **What you see:** The screenshot stops at the first screen, or ends partway down with the footer missing.
- **Why it happens:** By default a capture is the viewport, not the page: `fullPage` is off in both Playwright and Puppeteer until you set it. Tools also cap the height of a full-page image, so an endless feed cannot produce an endless file. A third cause: the content scrolls inside an inner container, so the document itself is only one screen tall. [8][10]
- **Fix it by hand:** Choose the full-page option, not the visible area. Capture a very long page in sections.
- **Fix it in an automated capture:** Turn full-page capture on and know your tool's height limit. For an inner scroll container, give the viewport enough height that the container no longer needs to scroll.
- **In CurlShot:** `full_page=true`. The cut-off height is `full_page_max_height`: 20,000 pixels by default, 30,000 at most, and a plan can set a lower ceiling. A taller page is cut off there; the request does not fail. See [Very tall pages](https://curlshot.com/docs/full-page).

### 6. Fallback fonts

- **What you see:** Headings and text are in a default serif or system font, lines wrap differently from the live site, or text is missing altogether.
- **Why it happens:** A web font is a separate download. During the font block period the browser draws the text invisibly, then with a fallback font until the real one arrives; how long each phase lasts depends on the page's `font-display` value. A capture in that window records the fallback. `document.fonts.ready` resolves only once font loading and the layout that follows are finished. [5][6]
- **Fix it by hand:** Wait until the text settles, or reload once so the fonts come from the cache.
- **Fix it in an automated capture:** Await `document.fonts.ready` in the page before capturing. If the font never arrives, look for a failed font request in the network log.
- **In CurlShot:** There is no font-specific option. `wait_until=networkidle` or a short `delay` covers a late font. Check that the request does not send `block_resources=font`, which draws the page in fallback fonts on purpose. See [Waiting and timing](https://curlshot.com/docs/waiting).

### 7. Frozen mid-animation

- **What you see:** Sections are half transparent, shifted by a few pixels or missing, and two captures of the same page do not match.
- **Why it happens:** A screenshot records a single frame. Entrance animations start on load or when a section scrolls into view, so a capture taken during one freezes it partway. Playwright's `animations: "disabled"` shows the clean fix: finite animations are fast-forwarded to their end and infinite ones are reset to their start. A site that honours `prefers-reduced-motion` skips the motion when the browser reports that preference. [8][11][3]
- **Fix it by hand:** Wait for the motion to finish. The reduce motion setting of your system stops it on sites that respect the setting.
- **Fix it in an automated capture:** Disable CSS animations and transitions at capture time, emulate reduced motion, and scroll through the page first so scroll-triggered reveals have already played.
- **In CurlShot:** `reduced_motion=true` reports the preference to the page and, for image captures, stops CSS animations and transitions at the moment of capture. Add a short `delay` for motion driven by JavaScript.

### 8. Wrong layout for the device

- **What you see:** A phone-sized screenshot shows the desktop site shrunk to tiny text, or a desktop screenshot shows the mobile menu.
- **Why it happens:** Layout follows the viewport width, and a narrow window alone is not a phone. Mobile browsers read the viewport meta tag; without mobile emulation, or on a page that lacks `width=device-width`, the page can be laid out in a virtual viewport of around 980 pixels and shrunk to fit. Some sites also pick a layout from the user agent. [12]
- **Fix it by hand:** Use the browser's device mode and pick a device, rather than dragging the window narrower.
- **Fix it in an automated capture:** Set width, height, mobile emulation, touch and user agent together. A device preset does all of it in one setting.
- **In CurlShot:** `viewport_device=iphone_15_pro` sets size, density, mobile mode and user agent at once. By hand: `viewport_width`, `viewport_height` and `viewport_mobile=true`. With none of them the window is 1280 by 1024, desktop. See [Device presets](https://curlshot.com/docs/devices).

### 9. Blurry text

- **What you see:** Text and icons look soft or pixelated next to the live page, most of all on a laptop or phone screen.
- **Why it happens:** `devicePixelRatio` is the ratio of physical pixels to CSS pixels: 1 on a classic 96 DPI display, 2 on HiDPI and Retina screens. A capture at scale 1 holds one image pixel per CSS pixel, so a 2x screen has to stretch each one over four physical pixels. Image width = viewport width x scale, so 1280 x 2 = 2560 pixels. [13][14]
- **Fix it by hand:** Capture on a high-density screen, or add a device pixel ratio of 2 in DevTools device mode before capturing.
- **Fix it in an automated capture:** Set the device scale factor to 2, or 3 for a phone. Enlarging a scale 1 image afterwards does not bring the detail back.
- **In CurlShot:** `device_scale_factor` takes 1 to 3 and the default is 1. Scale 2 makes 2 x 2 = 4 times the pixels, so use JPEG or WebP for tall pages. A plan can cap the scale of full-page captures. See [High-resolution screenshots](https://curlshot.com/tools/high-resolution-screenshot).

### 10. Login wall

- **What you see:** The screenshot shows a sign-in form or a "please log in" page, not the dashboard you asked for.
- **Why it happens:** A session lives in a cookie or a token held by your own browser. An automated browser starts empty, so the site redirects it to the sign-in page, and the capture is a faithful screenshot of that.
- **Fix it by hand:** Sign in first in the same browser, then capture.
- **Fix it in an automated capture:** Give the browser a session before it loads the page: a session cookie, a token header or HTTP credentials. Use an account made for this job, with read-only rights.
- **In CurlShot:** `cookies` for a session cookie, `headers` for a token, `authorization` for HTTP authentication. Send them in a POST body so they never sit in a web address. See [Screenshot behind a login](https://curlshot.com/tools/screenshot-behind-login).

### 11. Bot-check page

- **What you see:** The screenshot shows a page that says it is checking your browser, a CAPTCHA, or an "Access denied" message.
- **Why it happens:** Some sites put a challenge in front of visitors to decide whether they are human. Cloudflare, for one, describes a challenge as asking the browser to perform a series of checks. An automated browser is exactly what such a check looks for, so it is shown the challenge and the capture records it. [15]
- **Fix it by hand:** Open the page in your normal browser, pass the check, and capture there.
- **Fix it in an automated capture:** If the site is yours, allow your capture tool through in the firewall or bot settings, or capture a staging copy. If it is not yours, ask the owner or use an API they offer. Working around a check can break the site's terms.
- **In CurlShot:** No option removes a bot check, and CurlShot does not try to get around one: the screenshot shows the challenge page. On your own site, allow the service through, or send a secret with `headers` that your firewall accepts.

## Page load timeline: when is a page ready for a screenshot?

A page becomes ready in stages, and a capture taken at each stage fails in its own way.

| Moment | What has happened | A capture taken here shows |
| --- | --- | --- |
| 1. Response starts | The server has begun to answer and the document starts loading. Playwright calls this `commit`. [8] | A blank page. |
| 2. DOMContentLoaded | The page DOM has been loaded, without waiting for resources to finish loading. [7] | Text and structure, with images still missing. |
| 3. load | The page and its dependent resources are loaded: stylesheets, scripts, iframes and images, except those loaded lazily. [7] | Spinners where scripts fetch data, and empty boxes for lazy images. |
| 4. Network idle | No network connections for at least 500 ms (Puppeteer's `networkidle2` allows 2). A page that polls or streams never gets here. [8][9] | Usually the data, but lazy images below the first screen are still empty. |
| 5. Fonts ready | The `fonts.ready` promise of the document resolves: font loading and the layout after it are complete. [6] | Before this, fallback fonts or invisible text. |
| 6. Lazy content loaded | Happens only after something scrolls each lazy image or section near the viewport. [1][3] | Before this, a full-page capture has gaps further down. |
| 7. Animations settled | Entrance and scroll-triggered animations have reached their final frame. | The page as a visitor sees it. This is the moment to capture. |

The first four moments always come in this order. Fonts, lazy content and animations finish in an order that depends on the page, which is why one waiting rule never fits every site.

## Pre-flight checklist before any website screenshot

1. **Decide the device.** Set the viewport width and height on purpose. The default window of a tool is rarely the one your reader uses. (CurlShot: `viewport_device`, `viewport_width`, `viewport_height`)
2. **Turn on mobile emulation for a phone layout.** A narrow window is not a phone. Emulation makes the browser follow the viewport meta tag and report touch. (CurlShot: `viewport_mobile`)
3. **Set the pixel density.** Use a device scale factor of 2 for anything shown on modern screens. Image width = viewport width x scale. (CurlShot: `device_scale_factor`)
4. **Choose first screen or full page.** If full page, find out the height limit of your tool and what happens beyond it. (CurlShot: `full_page`, `full_page_max_height`)
5. **Name a readiness signal.** Best: a selector of the real content. Next: network idle. Last: a fixed delay, which slows every capture. (CurlShot: `wait_for_selector`, `wait_until`, `delay`)
6. **Scroll the page once before a full-page capture.** Scrolling triggers lazy images and scroll-driven reveals. Return to the top before capturing. (CurlShot: `full_page_scroll`)
7. **Clear the overlays.** Cookie banner, chat bubble, newsletter pop-up, sticky header. Block them, close them or hide them by selector. (CurlShot: `block_cookie_banners`, `block_chats`, `hide_selectors`)
8. **Stop the motion.** Disable CSS animations and transitions, and ask the page for reduced motion. (CurlShot: `reduced_motion`)
9. **Wait for the fonts.** Await `document.fonts.ready` in your own script, or allow the network to go quiet. (CurlShot: `wait_until`, `delay`)
10. **Bring the state the page needs.** A session cookie or token for pages behind a login, plus anything else the page reads: theme, language, time zone. (CurlShot: `cookies`, `headers`, `authorization`, `time_zone`)
11. **Look at the result before you trust it.** Check the first capture by eye for a login form, a bot check or a cut-off edge, then automate the rest.

## Each fix as a CurlShot API option

| Option | Fixes | Default | Allowed |
| --- | --- | --- | --- |
| `full_page` | Cut-off page | false | true or false |
| `full_page_scroll` | Blank image boxes | true | Used with full_page |
| `full_page_max_height` | Cut-off page | 20000 px | 100 to 30000 px |
| `wait_for_selector` | Spinners and skeletons | Not set | One CSS selector; it must be visible |
| `wait_until` | Spinners, late fonts | load | commit, domcontentloaded, load, networkidle |
| `delay` | Late fonts, animations | 0 s | 0 to 30 s |
| `block_cookie_banners` | Cookie banner on top | false | true or false |
| `hide_selectors` | Sticky header, any overlay | None | Up to 50 CSS selectors |
| `reduced_motion` | Frozen mid-animation | false | true or false |
| `viewport_device` | Wrong layout | Not set | A preset name, such as iphone_15_pro |
| `viewport_width`, `viewport_height` | Wrong layout | 1280 x 1024 | 100 to 3840 wide, 100 to 4320 high |
| `viewport_mobile` | Wrong layout | false | true or false |
| `device_scale_factor` | Blurry text | 1 | 1 to 3 |
| `cookies`, `headers`, `authorization` | Login wall | Not set | Up to 50 cookies, up to 50 headers |

One request with the common fixes applied:

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY\
&url=https://example.com\
&full_page=true\
&wait_until=networkidle\
&block_cookie_banners=true\
&hide_selectors=.site-header\
&reduced_motion=true\
&device_scale_factor=2" \
  --output page.png
```

Details: [Waiting and timing](https://curlshot.com/docs/waiting), [Blocking ads, cookie banners, trackers and chats](https://curlshot.com/docs/blocking), [Screenshot without cookie banners](https://curlshot.com/tools/screenshot-without-cookie-banners). No option removes a bot check.

## Glossary

- **Viewport:** The browser window a page is drawn in. Its width decides which layout a responsive page shows.
- **Full-page screenshot:** A capture of the whole scrollable page from top to bottom, not only the part visible in the viewport.
- **Lazy loading:** Holding back an image or a section until the visitor scrolls near it, to make the first load faster.
- **load event:** The browser event fired when the page and its dependent resources have loaded, except those loaded lazily.
- **Network idle:** In Playwright and Puppeteer, the moment when there have been no network connections for at least 500 ms.
- **Device pixel ratio:** Physical screen pixels per CSS pixel: 1 on a classic 96 DPI display, 2 on HiDPI and Retina screens. Also called device scale factor.
- **Fixed and sticky positioning:** CSS that pins an element to the viewport (fixed) or keeps it in view inside its scrolling ancestor (sticky) while the page scrolls.
- **Fallback font:** The font a browser draws text in while the web font it was asked for has not loaded, or has failed to load.

## Questions

### Why are images blank in my full-page screenshot?

The images are lazy loaded: the browser fetches them only when they come near the viewport, and the load event does not wait for them. Scroll through the whole page before capturing, pause so the images can arrive, then return to the top and take the screenshot.

### Why does the header repeat in my full-page screenshot?

The header is a fixed or sticky element, pinned to the viewport. A tool that scrolls, captures each screen and stitches the pieces records it once per piece. Hide the header or set it to position: static before the capture, or use a tool that renders the page in one pass.

### How do I take a screenshot without the cookie banner?

By hand, answer the banner first. In an automated capture, set the consent cookie, click the button, block the consent script or hide the banner with CSS. In CurlShot, add block_cookie_banners=true, and hide_selectors for a banner it does not recognise.

### Is waiting for network idle enough before a screenshot?

Not always. Network idle means no network connections for at least 500 ms. It does not cover lazy images that need a scroll, fonts still swapping in, or animations, and a page that polls never becomes idle. Waiting for a specific element of the content is more exact.

### Why is my screenshot blurry?

It was captured at a device scale factor of 1, one image pixel per CSS pixel, and is being shown on a screen with a device pixel ratio of 2 or more. Capture at scale 2: a 1280 pixel wide viewport then gives a 2560 pixel wide image.

### Why does my screenshot show a login page or a CAPTCHA?

An automated browser starts with no session, so a protected page redirects it to the sign-in form; send a session cookie or token with the request. A CAPTCHA or "checking your browser" page is a bot check. CurlShot has no option that removes one: allow the capture tool on your own site, or ask the site's owner.

## Sources

1. MDN Web Docs, "<img>: the image embed element (loading attribute)": https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img (checked 11 October 2026)
2. MDN Web Docs, "Lazy loading": https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Lazy_loading (checked 11 October 2026)
3. MDN Web Docs, "Intersection Observer API": https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API (checked 11 October 2026)
4. MDN Web Docs, "position (CSS property)": https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/position (checked 11 October 2026)
5. MDN Web Docs, "font-display (@font-face descriptor)": https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@font-face/font-display (checked 11 October 2026)
6. MDN Web Docs, "FontFaceSet: ready property": https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready (checked 11 October 2026)
7. MDN Web Docs, "Window: load event": https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event (checked 11 October 2026)
8. Playwright, "Page class: goto waitUntil and screenshot options": https://playwright.dev/docs/api/class-page (checked 11 October 2026)
9. Puppeteer, "PuppeteerLifeCycleEvent type": https://pptr.dev/api/puppeteer.puppeteerlifecycleevent (checked 11 October 2026)
10. Puppeteer, "ScreenshotOptions interface": https://pptr.dev/api/puppeteer.screenshotoptions (checked 11 October 2026)
11. MDN Web Docs, "prefers-reduced-motion (media feature)": https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion (checked 11 October 2026)
12. MDN Web Docs, "Viewport meta element": https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Viewport_meta_element (checked 11 October 2026)
13. MDN Web Docs, "Window: devicePixelRatio property": https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio (checked 11 October 2026)
14. Chrome for Developers, "Simulate mobile devices with device mode": https://developer.chrome.com/docs/devtools/device-mode (checked 11 October 2026)
15. Cloudflare Docs, "Challenges": https://developers.cloudflare.com/cloudflare-challenges/ (checked 11 October 2026)

## Go deeper

- [Waiting and timing](https://curlshot.com/docs/waiting)
- [Blocking ads, cookie banners, trackers and chats](https://curlshot.com/docs/blocking)
- [Screenshot without cookie banners](https://curlshot.com/tools/screenshot-without-cookie-banners)
- [Screenshot without ads](https://curlshot.com/tools/screenshot-without-ads)
- [Full-page screenshot tool](https://curlshot.com/tools/full-page-screenshot)
- [Full-page screenshots](https://curlshot.com/docs/full-page)

## Read next

- [Full-page screenshot on every device and browser](https://curlshot.com/blog/full-page-screenshot-every-device.md): Exact keys and menu names to screenshot a whole web page in Chrome, Firefox, Edge, Safari, iPhone, iPad and Android, checked against each vendor.
- [Viewport sizes cheat sheet: phones to desktops](https://curlshot.com/blog/viewport-sizes-cheat-sheet.md): Viewport sizes in CSS pixels, screenshot sizes and device pixel ratios of iPhone, Pixel, Galaxy, iPad and laptop screens, drawn to scale, and the sizes to test.
