Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait for two different conditions before capturing a Web Component: first, its custom-element definition must be registered; second, the component must expose a state that means its content is visually ready. In Python, Playwright provides the most direct implementation: call customElements.whenDefined(), then use Locator.wait_for_function() for the component’s readiness contract, and only then call screenshot().

Why page-load waits are not enough

page.goto(), Selenium’s readyState, and network-idle waits describe document loading, not application rendering. JavaScript can register a custom element after navigation, fetch data later, decode images asynchronously, or update a shadow tree after the initial DOM is present. Selenium explicitly notes that readyState concerns assets declared in HTML while JavaScript can continue changing the page (Waiting Strategies).

A custom element also has two separate milestones:

  • Defined/upgraded: the browser has run customElements.define() for the tag.
  • Visually ready: the component’s own data, images, layout, and state are ready for capture.

customElements.whenDefined(name) resolves only for the first milestone. MDN documents it as a Promise that resolves when the named element is defined (MDN reference); it does not guarantee that asynchronous rendering has finished.

Recommended Playwright Python implementation

Install Playwright and its browser binaries once:

python -m pip install playwright
python -m playwright install chromium

This complete script waits for registration, then for a component-owned data-ready="true" marker, and captures only the component.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError

URL = "https://example.com"
TAG = "my-widget"
OUTPUT = "widget.png"

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1440, "height": 900}, device_scale_factor=1)
    try:
        page.goto(URL, wait_until="domcontentloaded", timeout=60_000)
        widget = page.locator(TAG)
        widget.wait_for(state="attached", timeout=30_000)

        # Gate 1: the browser has upgraded the custom element.
        page.wait_for_function(
            "tag => customElements.whenDefined(tag)",
            TAG,
            timeout=30_000,
        )

        # Gate 2: replace this with the component's documented readiness signal.
        widget.wait_for_function(
            "el => el.getAttribute('data-ready') === 'true'",
            timeout=30_000,
        )

        # Playwright performs actionability checks and scrolls the locator into view.
        widget.screenshot(path=OUTPUT)
    except PlaywrightTimeoutError as error:
        print(f"Timed out waiting for {TAG} on {URL}: {error}")
        raise
    finally:
        browser.close()

Playwright’s locator.wait_for_function() retries a custom predicate while re-resolving the locator, making it suitable for application-level state (Locator API). Its screenshot action performs actionability checks and scrolls the target into view before capture.

Use the correct readiness marker

data-ready is an example, not a universal attribute. Use a marker that the component actually sets:

  • A documented attribute such as data-ready="true" or aria-busy="false".
  • A stable child that appears only after rendering, for example widget.locator(".results").wait_for(state="visible").
  • A text value guaranteed to follow successful loading.
  • An application event that sets a host attribute your test can observe.

Do not invent a marker. If the component has no readiness contract, add one to the component rather than guessing from timing.

Waiting for definition and upgrade correctly

Custom-element names must contain a hyphen, such as my-widget. The defining script must load and call customElements.define("my-widget", MyWidget). The following gate waits for registration even when the script executes after navigation:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
page.wait_for_function(
    "tag => customElements.whenDefined(tag)",
    "my-widget",
)

You can also evaluate the Promise directly:

page.evaluate("tag => customElements.whenDefined(tag)", "my-widget")

Registration is not the same as completed rendering. A component’s connectedCallback() runs when its host is connected, but asynchronous work may still be pending. The HTML Standard and MDN Web Components documentation describe connection and lifecycle callbacks, while leaving application-level visual readiness to the component author (Web Components, Using custom elements, WHATWG HTML Standard).

Common readiness contracts

Attribute or ARIA state

widget.wait_for_function(
    "el => el.getAttribute('aria-busy') === 'false'",
    timeout=30_000,
)

This is usually the most stable contract because it is external to implementation details.

Rendered child

widget.locator(".chart-canvas").wait_for(state="visible", timeout=30_000)

Choose a child whose presence means the result is complete, not merely a loading shell.

Open shadow DOM

For an open shadow root, inspect a stable shadow child:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
widget.locator(".status").wait_for(state="visible")

Playwright can pierce open shadow DOM selectors. A closed shadow root is intentionally inaccessible to page scripts and automation; require a host-level attribute, event-driven state copied to the host, or another public signal.

Images and fonts

If the component marks itself ready before images decode, wait for the relevant image state in a custom predicate:

widget.wait_for_function("""
el => [...el.querySelectorAll('img')].every(img => img.complete && img.naturalWidth > 0)
""", timeout=30_000)

Use this only when those images are part of the component’s actual contract. A generic network-idle wait is not proof that pixels are final.

Selenium alternative

If your project already uses Selenium, wait on the same component-owned condition instead of relying on navigation completion:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait

URL = "https://example.com"
wait_seconds = 30

driver = webdriver.Chrome()
try:
    driver.get(URL)
    wait = WebDriverWait(driver, wait_seconds)
    wait.until(lambda d: d.execute_script("""
        const el = document.querySelector('my-widget');
        return el && el.getAttribute('data-ready') === 'true';
    """))
    driver.save_screenshot("widget.png")
finally:
    driver.quit()

Playwright generally offers a more convenient custom-predicate and locator screenshot workflow, but Selenium may be preferable when your organisation already standardises its drivers, browser matrix, and diagnostics.

Capture the whole page instead of one component

After the same two gates, capture the document:

page.screenshot(path="page.png", full_page=True)

For a component, widget.screenshot() avoids unrelated page changes and naturally focuses the output. Set a deterministic viewport, timezone, locale, and device scale factor when comparing screenshots in CI.

Timeouts, diagnostics, and recovery

The wait times out

  • Log the URL, tag name, timeout, and the last observed readiness value.
  • Capture an evidence screenshot or save page.content() before closing the browser.
  • Check whether the component deliberately reports an error state that your predicate ignores.

The element never upgrades

  • Confirm the tag contains a hyphen.
  • Verify the module or script request succeeded and that customElements.define() ran.
  • Check browser-console errors and module paths.
  • Ensure the locator is not inside a different frame; switch to the correct frame_locator when necessary.

The screenshot is blank or stale

  • Make the predicate observe rendered state, not just element existence.
  • Check that CSS is loaded and that the element is not hidden by a parent.
  • Wait for image decoding when images are part of the output.
  • Disable or finish animations for repeatable captures; otherwise two screenshots can represent different animation frames.

Closed shadow root

Do not attempt to query private internals. Ask the component to expose a public readiness attribute or event, then wait on that host-level signal.

Frames and cross-origin content

A custom element inside an iframe must be located through that frame. Cross-origin iframe internals remain subject to browser isolation; capture the frame or coordinate with the embedded application for a readiness signal.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Performance and reliability practices

  • Use domcontentloaded for navigation, then let the component predicate determine completion.
  • Choose a bounded timeout (30 seconds is a practical starting point) and fail rather than silently capturing partial UI.
  • Reuse a browser process for batches of URLs, but create isolated contexts when cookies, locale, or authentication differ.
  • Wait on the smallest useful locator to reduce accidental dependence on unrelated page activity.
  • Record browser version, URL, selector, and readiness state with each failure.
  • For visual regression, freeze data, viewport, fonts, timezone, and animation state.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. It can accept consent banners before capture and remove more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status.

For a URL that exposes a reliable rendered page, call the API directly:

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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
require('fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));

See the parameter reference and readiness-related options in the ScreenshotNeo documentation. The service supports waits for selectors, delays, or network idle; custom JavaScript and CSS; click and hide actions; full-page or CSS-element capture; 12 device presets or arbitrary viewports; retina scale; dark mode; headers, cookies, user agents, authorization, timezone, and geolocation; ad, tracker, request, and resource blocking; image resizing; transparent backgrounds; chosen cache TTLs; signed image links; asynchronous jobs with signed webhooks; PDF output; bulk capture of up to 100 URLs per call; usage data; and an OpenAPI specification. Parameters used by other screenshot APIs also work, easing migration.

ScreenshotNeo also offers take_screenshot, get_page_info, and capture_pdf through its MCP server for Claude, Cursor, and other MCP clients. Every plan includes every feature: Free provides 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots, with yearly billing giving two months free.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

Which approach should you choose?

Need Best fit Reason
A custom element with a documented readiness marker Playwright Python Direct locator predicate and component screenshot API
An existing Selenium test suite Selenium Reuse established drivers and WebDriver infrastructure
Many URLs, cleanup, PDFs, or AI-agent capture ScreenshotNeo Browser setup is replaced by an API or MCP call; only clean shots are billed

FAQ

Does whenDefined() wait for data?

No. It waits for registration and upgrade only; data and visual rendering require a second condition.

Can I use a fixed sleep?

You can, but it is slower when pages are fast and flaky when pages are slow. A component-owned predicate is both faster and more deterministic.

What if the component has no readiness signal?

Add a public marker or event if you control it. Otherwise wait for a stable, documented rendered outcome and treat the result as less reliable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Does network idle guarantee that a custom element is ready?

No. Rendering, image decoding, timers, and application state can change after network activity stops; wait for the component’s own observable state.

Can Playwright screenshot a closed shadow root?

It can capture the host’s pixels, but automation cannot inspect closed internals. Expose readiness on the host or through a public event.

How should CI handle a readiness timeout?

Fail the capture, record the URL, selector, timeout and last state, and preserve diagnostic HTML or a screenshot instead of publishing a partial image.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.