Why website screenshots look wrong, and how to fix each case
A field guide to the 11 ways a capture of a web page goes wrong. Spot the symptom, read what the browser was doing, and apply the fix, by hand or in an automated capture.
- 1.Blank image boxes
- 2.Spinners and skeletons
- 3.Cookie banner on top
- 4.Repeated sticky header
- 5.Cut-off page
- 6.Fallback fonts
- 7.Frozen mid-animation
- 8.Wrong layout for the device
- 9.Blurry text
- 10.Login wall
- 11.Bot-check page
- The page as it should look
What is wrong with my screenshot? A diagnosis table
A broken website screenshot is nearly always an accurate record of the page at the wrong moment or in the wrong state. Find what you see in the first column and follow the link to its entry.
| 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 of a broken screenshot, with the cause and fix for each
Each entry shows the symptom as a small wireframe page: grey bars are text, tinted boxes are images. The last line of an entry names the option in CurlShot that applies the fix, where one exists.
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 theloadevent does not wait for it. Scripts built onIntersectionObserverhold 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
imgreportscomplete. - In CurlShot
full_page=truescrolls through the page first. That step isfull_page_scroll, on by default. The scroll has a time budget, so on a very long page lowerfull_page_max_height. Full-page screenshots
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
loadevent 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 afterload, so a capture atloadshows 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_selectorwaits until the element is visible.wait_until=networkidleis the fallback, anddelayadds up to 30 seconds after both. Waiting and timing
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: fixedelement, 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=trueblocks 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 addhide_selectors,clickorcookies. 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: fixedelement is placed relative to the viewport, and aposition: stickyelement 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.[14]
- Fix it in an automated capture
- Before the capture, hide the element or switch it to
position: staticwith an injected style rule, so it scrolls away with the page. - In CurlShot
hide_selectors=.site-headerremoves the bar.stylestakes your own CSS if you would rather un-stick it than hide it. Sticky headers in full-page screenshots
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:
fullPageis 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 isfull_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. Very tall pages
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-displayvalue. A capture in that window records the fallback.document.fonts.readyresolves 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.readyin 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=networkidleor a shortdelaycovers a late font. Check that the request does not sendblock_resources=font, which draws the page in fallback fonts on purpose. Waiting and timing
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 honoursprefers-reduced-motionskips 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=truereports the preference to the page and, for image captures, stops CSS animations and transitions at the moment of capture. Add a shortdelayfor 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_prosets size, density, mobile mode and user agent at once. By hand:viewport_width,viewport_heightandviewport_mobile=true. With none of them the window is 1280 by 1024, desktop. Device presets
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
devicePixelRatiois 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_factortakes 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. High-resolution screenshots
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
cookiesfor a session cookie,headersfor a token,authorizationfor HTTP authentication. Send them in a POST body so they never sit in a web address. Screenshot behind a 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
headersthat your firewall accepts.
When is a page ready for a screenshot? The page load timeline
A web page becomes ready in stages, and a capture taken at each stage fails in its own way. The seven drawings below are one page at seven moments, with what a screenshot taken at that instant would show.
1. Response starts
The server has begun to answer and the document starts loading. Playwright calls this
commit.[8]Captured here: A blank page.
2. DOMContentLoaded
The page DOM has been loaded, without waiting for resources to finish loading.[7]
Captured here: 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]
Captured here: Spinners where scripts fetch data, and empty boxes for lazy images.
5. Fonts ready
The
fonts.readypromise of the document resolves: font loading and the layout after it are complete.[6]Captured here: Before this, fallback fonts or invisible text.
7. Animations settled
Entrance and scroll-triggered animations have reached their final frame.
Capture here: The page as a visitor sees it. This is the moment to capture.
Why a fixed delay is the weakest fix
Pre-flight checklist before any website screenshot
11 settings to get right before a capture, from the browser window to the final look at the result. Tick them off as you go. The names in code are the CurlShot options that set each one.
Each fix as a CurlShot API option
CurlShot runs the browser for you, so each fix in this guide is one option on the request. The table lists every option the entries name, with its default and what it accepts.
| 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 |
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.pngThe request captures the whole page, waits for the network to go quiet, removes the cookie banner and the sticky header, stops animations and doubles the pixel density. More in Waiting and timing and Blocking ads, cookie banners, trackers and chats, or try it without code on Screenshot without cookie banners.
What no option fixes
wait_until=networkidle or a short delay.Screenshot terms, defined
- 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 people ask
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). Checked .
- [2]MDN Web Docs, Lazy loading. Checked .
- [3]MDN Web Docs, Intersection Observer API. Checked .
- [4]MDN Web Docs, position (CSS property). Checked .
- [5]MDN Web Docs, font-display (@font-face descriptor). Checked .
- [6]MDN Web Docs, FontFaceSet: ready property. Checked .
- [7]MDN Web Docs, Window: load event. Checked .
- [8]Playwright, Page class: goto waitUntil and screenshot options. Checked .
- [9]Puppeteer, PuppeteerLifeCycleEvent type. Checked .
- [10]Puppeteer, ScreenshotOptions interface. Checked .
- [11]MDN Web Docs, prefers-reduced-motion (media feature). Checked .
- [12]MDN Web Docs, Viewport meta element. Checked .
- [13]MDN Web Docs, Window: devicePixelRatio property. Checked .
- [14]Chrome for Developers, Simulate mobile devices with device mode. Checked .
- [15]Cloudflare Docs, Challenges. Checked .
Read next
- How-toFull-page screenshot on every device and browserExact keys and menu names to screenshot a whole web page in Chrome, Firefox, Edge, Safari, iPhone, iPad and Android, checked against each vendor.
- Cheat sheetViewport sizes cheat sheet: phones to desktopsViewport 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.