The Complete Guide to Playwright Screenshots: From Basic Snaps to Complex Automation

1/27/2025

Hey There, Screenshot Enthusiasts! 👋

Laura and Heidi here! After spending way too much time wrestling with browser automation (we built SCRNIFY, remember?), we thought we'd share a complete guide on using Playwright for screenshots. Whether you're just starting or looking to level up your automation game, this guide's got you covered.

Getting Started: The Basics

First things first - let's get Playwright installed. Open your terminal and type:

npm init -y
npm install playwright

That's it! No really, that's all you need to start. Let's write our first screenshot code:

// screenshot.js
const { chromium } = require('playwright');

async function takeBasicScreenshot() {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://news.ycombinator.com');
  await page.screenshot({ path: 'screenshot.png' });
  await browser.close();
}

takeBasicScreenshot();

Now run it!

node screenshot.js

Let's break this down:

  • chromium.launch() starts a new browser instance
  • browser.newPage() opens a new tab
  • page.goto() navigates to our URL
  • page.screenshot() captures what we see
  • browser.close() cleans up after we're done

Making It Better: Screenshot Types

That basic example works, but let's add some options. Playwright gives us three main ways to capture screenshots:

// Different types of screenshots
async function takeVariousScreenshots() {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://news.ycombinator.com');

  // Full page screenshot
  await page.screenshot({
    path: 'full-page.png',
    fullPage: true
  });

  // Viewport only
  await page.screenshot({
    path: 'viewport.png'
  });

  // Specific element
  const element = await page.locator('.hnname');
  await element.screenshot({
    path: 'element.png'
  });

  await browser.close();
}

takeVariousScreenshots();

node screenshot.js

Format Matters: JPEG, PNG, or WebP?

Different scenarios need different formats. Here's how to handle each:

async function differentFormats() {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://news.ycombinator.com');

  // JPEG - Great for photos, smaller file size
  await page.screenshot({
    path: 'screenshot.jpg',
    type: 'jpeg',
    quality: 80  // 0-100, lower = smaller file
  });

  // PNG - Perfect for screenshots with text
  await page.screenshot({
    path: 'screenshot.png',
    type: 'png'
  });

  // WebP - Modern format with good compression
  // currently missing https://github.com/microsoft/playwright/issues/22984
  // await page.screenshot({
  //   path: 'screenshot.webp',
  //   type: 'webp',
  // });

  await browser.close();
}

differentFormats();

Pro tip: Use JPEG for images with lots of colors, PNG for text-heavy screenshots, and WebP when you need both quality and small size.

Advanced Configuration: Making It Production-Ready

Let's step up our game with some advanced configurations. First, let's create a reusable config:

// config.js
const VIEWPORT_SIZES = {
  mobile: { width: 375, height: 812 },
  tablet: { width: 768, height: 1024 },
  desktop: { width: 1920, height: 1080 }
};

const SCREENSHOT_TYPES = {
  fullPage: {
    fullPage: true,
    type: 'png'
  },
  viewport: {
    fullPage: false,
    type: 'jpeg',
    quality: 80
  },
  element: {
    type: 'png',
    omitBackground: true
  }
};

const TIMEOUTS = {
  navigation: 30000,
  waitForElement: 5000,
  screenshot: 10000
};

module.exports = {
  VIEWPORT_SIZES,
  SCREENSHOT_TYPES,
  TIMEOUTS
};

Now let's use these configurations in a more robust setup:

// screenshot-service.js
const { chromium } = require('playwright');
const { VIEWPORT_SIZES, SCREENSHOT_TYPES, TIMEOUTS } = require('./config');

class ScreenshotService {
  constructor(options = {}) {
    this.browser = null;
    this.context = null;
    this.headless = options.headless !== false;
  }

  async initialize() {
    this.browser = await chromium.launch({
      headless: this.headless
    });
    this.context = await this.browser.newContext({
      viewport: VIEWPORT_SIZES.desktop
    });
  }

  async takeScreenshot(url, options = {}) {
    if (!this.browser) {
      await this.initialize();
    }

    const page = await this.context.newPage();

    try {
      await page.goto(url, {
        timeout: TIMEOUTS.navigation,
        waitUntil: 'networkidle'
      });

      const screenshotConfig = {
        ...SCREENSHOT_TYPES[options.type || 'fullPage'],
        path: options.path || `screenshot-${Date.now()}.png`
      };

      await page.screenshot(screenshotConfig);
    } finally {
      await page.close();
    }
  }

  async close() {
    if (this.browser) {
      await this.browser.close();
      this.browser = null;
    }
  }
}

Handling Authentication

Most real-world scenarios require authentication. Here's how to handle it:

async function screenshotWithAuth() {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();

  // Navigate to login page
  await page.goto('https://example.com/login');

  // Fill in login form
  await page.fill('input[name="username"]', 'myuser');
  await page.fill('input[name="password"]', 'mypassword');
  await page.click('button[type="submit"]');

  // Wait for navigation
  await page.waitForNavigation();

  // Store authentication state
  await context.storageState({ path: 'auth.json' });

  // Now take screenshots while authenticated
  await page.goto('https://example.com/dashboard');
  await page.screenshot({ path: 'authenticated-page.png' });

  await browser.close();
}

Pro tip: You can reuse the authentication state:

async function reuseAuth() {
  const browser = await chromium.launch();
  const context = await browser.newContext({
    storageState: 'auth.json'
  });

  // Now you're already authenticated!
  const page = await context.newPage();
  await page.goto('https://example.com/dashboard');
  await page.screenshot({ path: 'still-authenticated.png' });
}

Handling Dynamic Content

Modern websites are tricky - content loads dynamically, and we need to wait for the right moment:

async function handleDynamicContent() {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto('https://twitter.com/playwright', {
    waitUntil: 'networkidle'
  });

  // Wait for specific content
  await page.waitForSelector('.tweet-content', {
    state: 'visible',
    timeout: 10000
  });

  // Wait for images to load
  await page.waitForLoadState('domcontentloaded');

  // Optional: wait a bit more for animations
  await page.waitForTimeout(1000);

  await page.screenshot({
    path: 'dynamic-content.png',
    fullPage: true
  });

  await browser.close();
}

Common Challenges and Solutions

Handling Cookie Banners

async function handleCookieBanner() {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto('https://example.com');

  // Check if cookie banner exists and click accept
  const cookieBanner = await page.locator('.cookie-banner');
  if (await cookieBanner.isVisible()) {
    await page.click('.accept-cookies');
    // Wait for banner to disappear
    await cookieBanner.waitFor({ state: 'hidden' });
  }

  await page.screenshot({ path: 'no-cookie-banner.png' });
  await browser.close();
}

Dealing with Lazy Loading

async function handleLazyLoading() {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto('https://infinite-scroll-example.com');

  // Scroll to load more content
  await page.evaluate(() => {
    window.scrollTo(0, document.body.scrollHeight);
  });

  // Wait for new content
  await page.waitForSelector('.lazy-loaded-content');

  await page.screenshot({
    path: 'fully-loaded.png',
    fullPage: true
  });

  await browser.close();
}

Best Practices

1. Resource Management

Always clean up your browser instances:

async function properResourceManagement() {
  let browser = null;
  try {
    browser = await chromium.launch();
    const page = await browser.newPage();
    await page.goto('https://example.com');
    await page.screenshot({ path: 'screenshot.png' });
  } catch (error) {
    console.error('Screenshot failed:', error);
  } finally {
    if (browser) {
      await browser.close();
    }
  }
}

2. Error Handling

async function robustErrorHandling() {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  try {
    await page.goto('https://example.com', {
      timeout: 30000,
      waitUntil: 'networkidle'
    });
  } catch (error) {
    if (error.name === 'TimeoutError') {
      console.error('Page load timed out');
      // Maybe try again or use a different strategy
    }
    throw error;
  }

  try {
    await page.screenshot({ path: 'screenshot.png' });
  } catch (error) {
    console.error('Screenshot failed:', error);
    // Handle screenshot failure
  }

  await browser.close();
}

Debugging Tips

Visual Debugging

async function debugWithVisuals() {
  // Launch in headed mode
  const browser = await chromium.launch({ headless: false, slowMo: 1000 });
  const page = await browser.newPage();

  // Enable debugging
  page.on('console', msg => console.log('Browser console:', msg.text()));

  await page.goto('https://example.com');

  // Pause for inspection
  await page.pause();

  await page.screenshot({ path: 'debug.png' });
  await browser.close();
}

Quick Reference

Common Commands

// Basic screenshot
await page.screenshot({ path: 'basic.png' });

// Full page
await page.screenshot({ path: 'full.png', fullPage: true });

// Element screenshot
await page.locator('.element').screenshot({ path: 'element.png' });

// Custom viewport
await page.setViewportSize({ width: 1920, height: 1080 });

// Wait for element
await page.waitForSelector('.content');

// Wait for network
await page.waitForLoadState('networkidle');

Common Issues and Solutions

  1. White/Blank Screenshots
// Wait for content to be visible
await page.waitForSelector('.content', { state: 'visible' });
  1. Missing Dynamic Content
// Wait for network idle
await page.goto(url, { waitUntil: 'networkidle' });
  1. Authentication Issues
// Save auth state
await context.storageState({ path: 'auth.json' });

// Reuse auth state
const context = await browser.newContext({
  storageState: 'auth.json'
});

Security Considerations

  1. Never store sensitive credentials in code
  2. Use environment variables for auth data
  3. Clear stored auth states after use
  4. Be careful with screenshot storage locations
  5. Consider implementing rate limiting

Further Reading and Resources

Official Resources:

Authentication & Testing:

Advanced Topics:

Community Resources:

Cheers! 🍻

That's it! You're now equipped to handle pretty much any screenshot scenario with Playwright. Remember, while building your own solution can be fun (and educational), there's no shame in using existing services (like SCRNIFY 😉) for production needs.

Happy capturing!

Laura & Heidi from Numero33 🇦🇹

Ready to Get Started?

Sign up now and start capturing stunning screenshots in minutes.

Sign Up Now