ScreenshotNeo

BlogHow-to

Generate HTML Email Preview Screenshots with Python and Playwright

Render an HTML email file in Playwright and save a preview screenshot. Compare full-page and element captures, handle assets, and keep visual reviews consistent.

By the ScreenshotNeo team4 October 20267 min read

To generate an HTML email preview screenshot with Python and Playwright, read the markup, load it into a browser page with page.set_content(), then save the rendered page with page.screenshot(). The example below captures the complete scrollable email as a PNG. This is a browser preview; it does not establish how the email will render in Gmail, Outlook, Apple Mail, or another mail client.

1. Install Playwright and its browser

Install the Python package and Chromium browser. The browser installation is a separate step because Playwright needs a browser binary to launch.

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

Save your email markup as email.html. Then create preview_email.py with this complete synchronous example:

from pathlib import Path
from playwright.sync_api import sync_playwright

html = Path("email.html").read_text(encoding="utf-8")

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 600, "height": 900})
    page.set_content(html)
    page.screenshot(path="preview.png", full_page=True)
    browser.close()

Run it from the directory containing both files:

python preview_email.py

The result is preview.png. The 600 by 900 viewport is an illustrative choice, not an email standard or universal recommendation. Pick dimensions that suit the review you want, and keep them fixed when comparing previews.

2. Choose what the screenshot should include

Playwright supports several useful capture shapes. Use the one that matches the question you are trying to answer.

Capture Example Use it for
Visible viewport page.screenshot(path="preview.png") A fixed-size view of the page’s current visible area.
Full page page.screenshot(path="preview.png", full_page=True) A tall capture of the full scrollable page.
One element page.locator(".email-container").screenshot(path="email-container.png") A crop around a specific matching element, such as the email wrapper.
Bytes image_bytes = page.screenshot() Passing the PNG data to another Python step without first saving a file.

For an element screenshot, the selector must match an element. If the locator matches nothing, the capture cannot be made. If it matches multiple elements, make the target unambiguous—for example, select a unique wrapper or use a more specific locator.

3. Set the viewport and browser engine deliberately

A page viewport controls the available layout width and height; it does not emulate an email application. Choose a width that makes the layout behavior you want to inspect visible. Record the width and height alongside screenshot baselines so later comparisons use the same framing.

Playwright’s Python library supports synchronous and asynchronous APIs and can launch Chromium, Firefox, or WebKit. Select an engine based on the browser engine you want to inspect. The documentation does not establish that any one of these engines represents a particular email client.

To use Firefox or WebKit, install the corresponding browser and change the launch call:

python -m playwright install firefox webkit
# Replace p.chromium.launch() with either:
browser = p.firefox.launch()
# or:
browser = p.webkit.launch()

For repeatable preview work, keep the engine, browser version, operating environment, headless setting, and viewport consistent. Playwright documents these as sources of visual variation.

4. Wait for remote email assets when needed

page.set_content() loads the supplied HTML into the page. If that HTML points to remote images, fonts, stylesheets, or other resources, their availability and timing can affect the result. If an image or font is missing, inspect the URL and whether the browser can reach it. For content that loads after the markup, wait for a relevant element or a deliberate short delay before capturing; avoid an arbitrary long wait when a specific readiness condition is available.

For example, wait for a known wrapper to appear before taking its screenshot:

page.set_content(html)
page.locator(".email-container").wait_for(state="visible", timeout=10000)
page.locator(".email-container").screenshot(path="email-container.png")

This assumes the HTML contains an element with that class. Change the selector to match your markup. A selector becoming visible confirms only that element’s visibility, not that every remote asset has finished loading.

5. Use the asynchronous API in asyncio projects

If the surrounding application already uses asyncio, use Playwright’s async API and await browser, page, content, screenshot, and close operations:

import asyncio
from pathlib import Path
from playwright.async_api import async_playwright

async def main():
    html = Path("email.html").read_text(encoding="utf-8")

    async with async_playwright() as p:
        browser = await p.chromium.launch()
        try:
            page = await browser.new_page(viewport={"width": 600, "height": 900})
            await page.set_content(html)
            await page.screenshot(path="preview.png", full_page=True)
        finally:
            await browser.close()

asyncio.run(main())

The try/finally ensures the browser is closed if page setup or capture raises an exception.

6. Keep screenshot comparisons useful

A one-off preview answers “what did this markup render like here?” A visual baseline workflow answers “did a later render change?” Playwright’s visual comparison guidance describes creating a reference screenshot, comparing later output, and applying a stylesheet to filter volatile content.

  • Use the same browser engine and version for reference and later captures.
  • Keep viewport dimensions and operating environment fixed.
  • Remove or mask content that changes on every render when it is irrelevant to the review.
  • Update a reference image deliberately when the design is meant to change.

Rendering can vary with operating system, browser version, settings, hardware, and headless mode. A pixel difference does not automatically indicate a markup regression if the capture environment changed.

7. Troubleshooting

Symptom Likely cause What to do
Playwright cannot launch Chromium The Python package is installed but its browser binary is missing. Run python -m playwright install chromium. Install the engine you actually launch.
The screenshot is blank or incomplete The supplied HTML is empty, the wrong file was read, or content/resources have not loaded. Check the path and file contents, then inspect the rendered page and resource availability. Wait for a relevant readiness condition where needed.
Remote images or fonts are absent The resource URL is unavailable to the browser, blocked, invalid, or still loading. Check the URL and network access; wait for a specific element or asset condition if loading is delayed.
Locator screenshot times out The selector does not match, the element is not visible, or it appears too late. Verify the selector against the markup and wait for the intended element. Use a unique selector.
Screenshot differs on another machine Browser version, OS, settings, hardware, or headless mode differs. Pin and record the capture environment and viewport before comparing baselines.
Preview does not match a real inbox A browser-rendered HTML page is being treated as an email-client rendering test. Use this screenshot as a browser preview only. Validate separately in the target mail clients when client-specific rendering matters.

8. Performance, reliability, and cost

For a single local HTML file, the basic workflow is just one browser launch, one page render, and one screenshot. Reusing a browser process for several previews can avoid repeated launches, but create a fresh page or context when previews need isolation. Close pages and browsers when finished so background browser processes do not accumulate.

Capture time depends on page complexity, remote resources, and chosen waits; no fixed duration is guaranteed. For reliable automated reviews, use explicit readiness conditions, keep the capture environment stable, and handle launch or navigation failures as errors rather than silently treating an empty image as a valid preview.

Playwright is an open-source automation library; the code above has no per-screenshot API charge. You are responsible for the compute and browser environment where it runs. Remote assets may have their own access, availability, or usage constraints.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Send one request with a URL to receive an image or PDF, and see the API documentation for its options. For an HTML email preview, host the rendered page at a URL the API can access, then capture that URL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/email-preview -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com/email-preview"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com/email-preview'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.

Sign up free for 1,000 screenshots a month, no card required.

FAQ

Does this prove how the email looks in Gmail or Outlook?

No. It captures a browser rendering of your HTML. The reviewed Playwright documentation does not claim equivalence with email-client rendering.

Should I use full-page or element capture?

Use full-page capture to review all scrollable content. Use an element screenshot when the email wrapper or another specific component is the subject.

Can I compare future previews with a reference?

Yes. Playwright’s visual comparison workflow supports reference screenshots and later comparisons. Keep the browser and capture environment consistent, and filter changing content when it is irrelevant.

Official references