Multi-Device Screenshot Auditor
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
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
- Launches a headless Chromium browser via Playwright
- Iterates through all target device profiles (or a subset you specify)
- Sets exact viewport size + device pixel ratio + user-agent for each device
- Navigates to the target URL and waits for network idle
- Captures a screenshot (viewport-only or full-page)
- Saves all images to
./screenshots/with device-slug filenames - Emits a
REPORT.mdtable: device, dimensions, DPR, file path, status
Tools Required
- Node.js 18+ (or 20+)
- Playwright:
npm install -D playwrightthennpx playwright install chromium - Run from a directory that has
playwrightinnode_modules— the script usesimport { chromium } from 'playwright'which requires a local install. If you prefer no install, usenpx playwright(see Quickstart).
Device Profiles (19 targets)
| Slug | Label | Width × Height | DPR | Category |
|---|---|---|---|---|
iphone-se | iPhone SE | 375 × 667 | 2 | Phone |
iphone-14 | iPhone 14 | 390 × 844 | 3 | Phone |
iphone-14-pro-max | iPhone 14 Pro Max | 430 × 932 | 3 | Phone |
pixel-7 | Pixel 7 | 412 × 915 | 2.6 | Phone |
galaxy-s8 | Galaxy S8 | 360 × 740 | 3 | Phone |
iphone-14-landscape | iPhone 14 Landscape | 844 × 390 | 3 | Phone (landscape) |
ipad-mini | iPad Mini | 768 × 1024 | 2 | Tablet |
ipad-pro-11 | iPad Pro 11" | 834 × 1194 | 2 | Tablet |
ipad-pro-12 | iPad Pro 12.9" | 1024 × 1366 | 2 | Tablet |
ipad-pro-landscape | iPad Pro Landscape | 1366 × 1024 | 2 | Tablet (landscape) |
macbook-air | MacBook Air 13" | 1280 × 800 | 2 | Laptop |
laptop-hd | Laptop HD | 1366 × 768 | 1 | Laptop |
macbook-pro-16 | MacBook Pro 16" | 1728 × 1117 | 2 | Laptop |
desktop-1080p | Desktop 1080p | 1920 × 1080 | 1 | Desktop |
desktop-1440p | Desktop 1440p | 2560 × 1440 | 1 | Desktop |
desktop-4k | Desktop 4K | 3840 × 2160 | 1 | Desktop |
ultrawide | Ultrawide 21:9 | 3440 × 1440 | 1 | Desktop |
tv-1080p | TV 1080p | 1920 × 1080 | 1 | TV |
tv-4k | TV 4K | 3840 × 2160 | 1 | TV |
Script Template
Save as screenshot-audit.mjs and run with node screenshot-audit.mjs:
#!/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
# 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)
# 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 loggedAUDIT_AUTH_COOKIE— injected via Playwright context, not loggedAUDIT_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:
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:
# 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:
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:
AUDIT_TIMEOUT=60000 AUDIT_URL=https://example.com node screenshot-audit.mjs
Output Structure
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:
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]
$20 more to next tier
Created by
Info
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>