active

Multi-Device Screenshot Auditor

Safe
User-Verified

Capture pixel-perfect screenshots of any website across 19 device profiles — from iPhone SE to 4K TV — using Playwright. Produces a labeled gallery + markdown report. Supports authenticated pages via storage-state, cookies, or Bearer token. Perfect for visual QA, responsive design audits, and sharing with AI agents for UI/UX feedback.

@api/multi-device-screenshot-auditor

playwright
screenshots
responsive
visual-testing
ui-audit
device-emulation
tool

Multi-Device Screenshot Auditor

Capture pixel-perfect screenshots of any website across 19 device profiles using Playwright — from iPhone SE to 4K TV. Each run produces a labeled image gallery and a markdown report. Screenshots are immediately useful to share with an AI agent for UI/UX diagnosis.

What It Does

  1. Launches a headless Chromium browser via Playwright
  2. Iterates through all target device profiles (or a subset you specify)
  3. Sets exact viewport size + device pixel ratio + user-agent for each device
  4. Navigates to the target URL and waits for network idle
  5. Captures a screenshot (viewport-only or full-page)
  6. Saves all images to ./screenshots/ with device-slug filenames
  7. Emits a REPORT.md table: device, dimensions, DPR, file path, status

Tools Required

  • Node.js 18+ (or 20+)
  • Playwright: npm install -D playwright then npx playwright install chromium
  • Run from a directory that has playwright in node_modules — the script uses import { chromium } from 'playwright' which requires a local install. If you prefer no install, use npx playwright (see Quickstart).

Device Profiles (19 targets)

SlugLabelWidth × HeightDPRCategory
iphone-seiPhone SE375 × 6672Phone
iphone-14iPhone 14390 × 8443Phone
iphone-14-pro-maxiPhone 14 Pro Max430 × 9323Phone
pixel-7Pixel 7412 × 9152.6Phone
galaxy-s8Galaxy S8360 × 7403Phone
iphone-14-landscapeiPhone 14 Landscape844 × 3903Phone (landscape)
ipad-miniiPad Mini768 × 10242Tablet
ipad-pro-11iPad Pro 11"834 × 11942Tablet
ipad-pro-12iPad Pro 12.9"1024 × 13662Tablet
ipad-pro-landscapeiPad Pro Landscape1366 × 10242Tablet (landscape)
macbook-airMacBook Air 13"1280 × 8002Laptop
laptop-hdLaptop HD1366 × 7681Laptop
macbook-pro-16MacBook Pro 16"1728 × 11172Laptop
desktop-1080pDesktop 1080p1920 × 10801Desktop
desktop-1440pDesktop 1440p2560 × 14401Desktop
desktop-4kDesktop 4K3840 × 21601Desktop
ultrawideUltrawide 21:93440 × 14401Desktop
tv-1080pTV 1080p1920 × 10801TV
tv-4kTV 4K3840 × 21601TV

Script Template

Save as screenshot-audit.mjs and run with node screenshot-audit.mjs:

js
#!/usr/bin/env node
/**
 * Multi-Device Screenshot Auditor
 * Usage: node screenshot-audit.mjs [options]
 *
 * Options (via environment variables):
 *   AUDIT_URL          Target URL (required)
 *   AUDIT_DEVICES      Comma-separated device slugs (default: all)
 *   AUDIT_FULL_PAGE    "true" for full-page screenshots (default: viewport only)
 *   AUDIT_AUTH_STATE   Path to Playwright storage state JSON (for authenticated pages)
 *   AUDIT_AUTH_COOKIE  Raw cookie string, e.g. "session=abc; csrf=xyz"
 *   AUDIT_AUTH_TOKEN   Bearer token (added as Authorization header)
 *   AUDIT_OUTPUT_DIR   Output directory (default: ./screenshots)
 *   AUDIT_PAGES        JSON array of paths to capture, e.g. '["/","/dashboard"]'
 *   AUDIT_TIMEOUT      Navigation timeout in ms (default: 30000)
 */

import { chromium } from 'playwright'
import { writeFileSync, mkdirSync, existsSync } from 'fs'
import { join, resolve } from 'path'

const DEVICES = [
  { slug: 'iphone-se',           label: 'iPhone SE',           width: 375,  height: 667,  dpr: 2,   ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1' },
  { slug: 'iphone-14',           label: 'iPhone 14',           width: 390,  height: 844,  dpr: 3,   ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },
  { slug: 'iphone-14-pro-max',   label: 'iPhone 14 Pro Max',   width: 430,  height: 932,  dpr: 3,   ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },
  { slug: 'pixel-7',             label: 'Pixel 7',             width: 412,  height: 915,  dpr: 2.6, ua: 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Mobile Safari/537.36' },
  { slug: 'galaxy-s8',           label: 'Galaxy S8',           width: 360,  height: 740,  dpr: 3,   ua: 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G950F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36' },
  { slug: 'iphone-14-landscape', label: 'iPhone 14 Landscape', width: 844,  height: 390,  dpr: 3,   ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },
  { slug: 'ipad-mini',           label: 'iPad Mini',           width: 768,  height: 1024, dpr: 2,   ua: 'Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1' },
  { slug: 'ipad-pro-11',         label: 'iPad Pro 11"',        width: 834,  height: 1194, dpr: 2,   ua: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },
  { slug: 'ipad-pro-12',         label: 'iPad Pro 12.9"',      width: 1024, height: 1366, dpr: 2,   ua: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },
  { slug: 'ipad-pro-landscape',  label: 'iPad Pro Landscape',  width: 1366, height: 1024, dpr: 2,   ua: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },
  { slug: 'macbook-air',         label: 'MacBook Air 13"',     width: 1280, height: 800,  dpr: 2,   ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
  { slug: 'laptop-hd',           label: 'Laptop HD',           width: 1366, height: 768,  dpr: 1,   ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
  { slug: 'macbook-pro-16',      label: 'MacBook Pro 16"',     width: 1728, height: 1117, dpr: 2,   ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
  { slug: 'desktop-1080p',       label: 'Desktop 1080p',       width: 1920, height: 1080, dpr: 1,   ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
  { slug: 'desktop-1440p',       label: 'Desktop 1440p',       width: 2560, height: 1440, dpr: 1,   ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
  { slug: 'desktop-4k',          label: 'Desktop 4K',          width: 3840, height: 2160, dpr: 1,   ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
  { slug: 'ultrawide',           label: 'Ultrawide 21:9',      width: 3440, height: 1440, dpr: 1,   ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
  { slug: 'tv-1080p',            label: 'TV 1080p',            width: 1920, height: 1080, dpr: 1,   ua: 'Mozilla/5.0 (SMART-TV; Linux; Tizen 6.0) AppleWebKit/538.1 (KHTML, like Gecko) Version/6.0 TV Safari/538.1' },
  { slug: 'tv-4k',               label: 'TV 4K',               width: 3840, height: 2160, dpr: 1,   ua: 'Mozilla/5.0 (SMART-TV; Linux; Tizen 6.0) AppleWebKit/538.1 (KHTML, like Gecko) Version/6.0 TV Safari/538.1' },
]

async function run() {
  const url = process.env.AUDIT_URL
  if (!url) throw new Error('AUDIT_URL is required')

  const targetDeviceSlugs = process.env.AUDIT_DEVICES
    ? process.env.AUDIT_DEVICES.split(',').map(s => s.trim())
    : null
  const devices = targetDeviceSlugs
    ? DEVICES.filter(d => targetDeviceSlugs.includes(d.slug))
    : DEVICES

  const fullPage = process.env.AUDIT_FULL_PAGE === 'true'
  const outputDir = resolve(process.env.AUDIT_OUTPUT_DIR || './screenshots')
  const timeout = parseInt(process.env.AUDIT_TIMEOUT || '30000', 10)
  const pages = process.env.AUDIT_PAGES
    ? JSON.parse(process.env.AUDIT_PAGES)
    : [new URL(url).pathname || '/']

  // Auth options
  const authState = process.env.AUDIT_AUTH_STATE   // path to Playwright storage state JSON
  const authCookie = process.env.AUDIT_AUTH_COOKIE  // raw cookie string
  const authToken = process.env.AUDIT_AUTH_TOKEN    // Bearer token

  mkdirSync(outputDir, { recursive: true })

  // --no-sandbox is required on Linux and in CI/Docker environments
  // It is safe to use when you control the content being rendered
  const browser = await chromium.launch({
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
  })
  const results = []

  for (const device of devices) {
    for (const pagePath of pages) {
      const pageUrl = pagePath.startsWith('http') ? pagePath : new URL(pagePath, url).href
      const fileSuffix = pages.length > 1 ? `-${pagePath.replace(/\//g, '_').replace(/^_/, '') || 'home'}` : ''
      const fileName = `${device.slug}${fileSuffix}-${device.width}x${device.height}.png`
      const filePath = join(outputDir, fileName)

      const contextOptions = {
        viewport: { width: device.width, height: device.height },
        deviceScaleFactor: device.dpr,
        userAgent: device.ua,
        ...(authState && existsSync(authState) ? { storageState: authState } : {}),
      }

      const context = await browser.newContext(contextOptions)

      // Inject auth cookie if provided (never logged)
      if (authCookie) {
        const cookieEntries = authCookie.split(';').map(c => {
          const [name, ...rest] = c.trim().split('=')
          return { name: name.trim(), value: rest.join('=').trim(), url: pageUrl }
        })
        await context.addCookies(cookieEntries)
      }

      const page = await context.newPage()

      // Inject auth token header if provided (never logged)
      if (authToken) {
        await page.setExtraHTTPHeaders({ Authorization: `Bearer ${authToken}` })
      }

      let status = 'ok'
      try {
        await page.goto(pageUrl, { waitUntil: 'networkidle', timeout })
        // Wait for fonts, lazy images, and CSS animations to settle
        await page.waitForTimeout(1000)
        await page.screenshot({ path: filePath, fullPage })
      } catch (err) {
        status = err.message.includes('timeout') ? 'timeout' : `error: ${err.message}`
      }

      results.push({ device: device.label, slug: device.slug, width: device.width, height: device.height, dpr: device.dpr, file: filePath, status, page: pagePath })
      console.log(`[${status === 'ok' ? '✓' : '✗'}] ${device.label} (${device.width}×${device.height}) → ${fileName}`)

      await context.close()
    }
  }

  await browser.close()

  // Write report
  const reportLines = [
    `# Screenshot Audit Report`,
    ``,
    `**URL:** ${url}`,
    `**Date:** ${new Date().toISOString()}`,
    `**Mode:** ${fullPage ? 'Full page' : 'Viewport only'}`,
    `**Devices:** ${devices.length} | **Pages:** ${pages.length} | **Total:** ${results.length}`,
    ``,
    `## Results`,
    ``,
    `| Device | Size | DPR | Page | File | Status |`,
    `|--------|------|-----|------|------|--------|`,
    ...results.map(r =>
      `| ${r.device} | ${r.width}×${r.height} | ${r.dpr} | ${r.page} | \`${r.file.split('/').pop()}\` | ${r.status === 'ok' ? '✅' : '❌ ' + r.status} |`
    ),
    ``,
    `## Usage with AI Agents`,
    ``,
    `Share screenshots with an AI agent for layout analysis:`,
    ``,
    '```',
    `# Share mobile screenshots for responsive feedback`,
    `ls ${outputDir}/iphone-*.png ${outputDir}/ipad-*.png`,
    ``,
    `# Share all desktop sizes`,
    `ls ${outputDir}/desktop-*.png ${outputDir}/macbook-*.png`,
    '```',
  ]
  writeFileSync(join(outputDir, 'REPORT.md'), reportLines.join('\n'))
  console.log(`\nReport: ${join(outputDir, 'REPORT.md')}`)
}

run().catch(err => { console.error(err); process.exit(1) })

Quick Start

bash
# Install dependencies (run once from your project directory)
npm install -D playwright
npx playwright install chromium

# Screenshot a public page across all devices
AUDIT_URL=https://example.com node screenshot-audit.mjs

# Screenshot only mobile devices
AUDIT_URL=https://example.com AUDIT_DEVICES=iphone-se,iphone-14,pixel-7 node screenshot-audit.mjs

# Authenticated page via Playwright storage state
AUDIT_URL=https://myapp.com AUDIT_AUTH_STATE=.auth/session.json node screenshot-audit.mjs

# Full-page screenshots of multiple paths
AUDIT_URL=https://example.com \
  AUDIT_FULL_PAGE=true \
  AUDIT_PAGES='["/","/pricing","/blog"]' \
  node screenshot-audit.mjs

# Screenshot localhost (dev server)
AUDIT_URL=http://localhost:3000 node screenshot-audit.mjs

Generating a Playwright Storage State (for auth)

bash
# Log in interactively and save session to .auth/session.json
npx playwright codegen --save-storage=.auth/session.json https://myapp.com/login

The saved .auth/session.json contains cookies and localStorage. Pass its path as AUDIT_AUTH_STATE. Never commit this file — add .auth/ to .gitignore.

Auth Credential Security

Credentials are never written to the report or any log file:

  • AUDIT_AUTH_STATE — only the file path appears in output; file contents are not logged
  • AUDIT_AUTH_COOKIE — injected via Playwright context, not logged
  • AUDIT_AUTH_TOKEN — injected via HTTP headers, not logged

For CI/CD, pass these as secrets (GitHub Actions secrets, Vercel env vars, etc.).

Troubleshooting

Cannot find package 'playwright' Run the script from a directory where Playwright is installed (node_modules/playwright exists). Either:

bash
npm install -D playwright && npx playwright install chromium
node screenshot-audit.mjs

Or install globally: npm install -g playwright && playwright install chromium

Chromium fails to launch on Linux / CI / Docker The script already includes --no-sandbox and --disable-setuid-sandbox which are required on Linux and in containerized environments. If you still get sandbox errors, also try:

bash
# In Docker, you may also need:
PLAYWRIGHT_CHROMIUM_SANDBOX=false node screenshot-audit.mjs

net::ERR_CONNECTION_REFUSED on localhost Make sure your dev server is running before starting the audit:

bash
npm run dev &   # Start dev server in background
AUDIT_URL=http://localhost:3000 node screenshot-audit.mjs

Screenshots are blank / all black Increase the settle timeout by setting a longer wait, or check that the page doesn't require JavaScript-heavy rendering that exceeds the default wait. The script already waits 1000ms after networkidle — for heavier apps you may want to adjust the page.waitForTimeout(1000) call.

Some devices time out Large viewports (4K, ultrawide) take longer to render. Increase the timeout:

bash
AUDIT_TIMEOUT=60000 AUDIT_URL=https://example.com node screenshot-audit.mjs

Output Structure

code
screenshots/
  iphone-se-375x667.png
  iphone-14-390x844.png
  iphone-14-pro-max-430x932.png
  pixel-7-412x915.png
  galaxy-s8-360x740.png
  iphone-14-landscape-844x390.png
  ipad-mini-768x1024.png
  ipad-pro-11-834x1194.png
  ipad-pro-12-1024x1366.png
  ipad-pro-landscape-1366x1024.png
  macbook-air-1280x800.png
  laptop-hd-1366x768.png
  macbook-pro-16-1728x1117.png
  desktop-1080p-1920x1080.png
  desktop-1440p-2560x1440.png
  desktop-4k-3840x2160.png
  ultrawide-3440x1440.png
  tv-1080p-1920x1080.png
  tv-4k-3840x2160.png
  REPORT.md

Using Screenshots with AI Agents

After capturing, share the screenshots with Claude (or any multimodal model) for analysis:

code
Here are screenshots of my website at different device sizes. Please review them and identify:
1. Any layout breakpoints that look broken or cramped
2. Text that's too small or hard to read on mobile
3. Elements that overflow or get cut off
4. Navigation/header issues on smaller screens
5. Any spacing or alignment problems

[attach: iphone-se-375x667.png, ipad-mini-768x1024.png, desktop-1080p-1920x1080.png]
Dormant$0/mo

$20 more to next tier

Info

Created March 11, 2026
Version 1.0.0
Tool-invoked
Terminal output

Embed

Add this skill card to any webpage.

<iframe src="https://skillslap.com/skill/dbaab0ac-a380-4f70-8f1e-c68951103eb0/embed"
        width="400" height="200"
        style="border:none;border-radius:12px;"
        title="SkillSlap Skill: Multi-Device Screenshot Auditor">
</iframe>