Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Capture screenshots of web apps during development using Playwright and PIL. Supports full-page captures, interactive states, and an iterate-on-crop workflow that avoids slow re-screenshots.
.claude/skills/ui-screenshots/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-17 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-15 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
Capture screenshots of web apps and graphical UIs during development to document visual changes.
Use this skill when you need to:
bashpip install playwright Pillow -q playwright install chromium
pythonfrom playwright.async_api import async_playwright async def capture(url="http://localhost:3000", out="screenshot-raw.png", width=1400, height=5000): async with async_playwright() as p: browser = await p.chromium.launch() page = await browser.new_page(viewport={"width": width, "height": height}) await page.goto(url, wait_until="networkidle") await page.wait_for_timeout(4000) # let charts/animations render await page.screenshot(path=out, full_page=True) await browser.close()
wait_until="networkidle" + wait_for_timeout(4000) ensures async charts loadfull_page=True captures the entire scrollable contentDo NOT try to get perfect crops via Playwright's clip parameter. It's unreliable with full-page captures.
pythonfrom PIL import Image img = Image.open("screenshot-raw.png") cropped = img.crop((left, top, right, bottom)) # adjust based on what you see cropped.save("screenshot-final.png")
pythonelement = page.locator("selector").first await element.hover() await page.wait_for_timeout(1000) # let tooltip appear await page.screenshot(path="screenshot-hover.png", full_page=True)
For "selected" state without hover effect, move the mouse away after clicking:
pythonawait element.click() await page.mouse.move(300, 300) # move away so hover doesn't show await page.wait_for_timeout(500) await page.screenshot(path="screenshot-selected.png", full_page=True)
Crop different sections from a single full-page screenshot:
pythonimg.crop((0, 200, 920, 900)).save("screenshot-header.png") img.crop((0, 900, 920, 1600)).save("screenshot-main.png")
git checkout HEAD~1 -- <files> to revert, screenshot, then git checkout HEAD -- <files> to restoredevice_scale_factor=1 in Playwright to force 1x pixels so screenshots match what users see at 100% zoomFor desktop apps (VS, WPF, WinForms, console apps, terminals) where Playwright can't reach.
Find a window by title via Win32 API, capture its region with mss. Tested at ~33ms per capture.
pythonimport ctypes from ctypes import c_int, Structure, byref, windll import mss from PIL import Image user32 = windll.user32 def find_window(title_contains): """Find visible windows matching a title substring.""" results = [] WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p) def cb(hwnd, _): if user32.IsWindowVisible(hwnd): buf = ctypes.create_unicode_buffer(256) user32.GetWindowTextW(hwnd, buf, 256) if title_contains.lower() in buf.value.lower(): results.append((hwnd, buf.value)) return True user32.EnumWindows(WNDENUMPROC(cb), 0) return results def capture_window(title_contains, output_path): """Capture a window by title substring.""" windows = find_window(title_contains) if not windows: raise ValueError(f"No window matching '{title_contains}'") hwnd = windows[0][0] class RECT(Structure): _fields_ = [('left', c_int), ('top', c_int), ('right', c_int), ('bottom', c_int)] rect = RECT() user32.GetWindowRect(hwnd, byref(rect)) w, h = rect.right - rect.left, rect.bottom - rect.top with mss.mss() as sct: shot = sct.grab({'left': rect.left, 'top': rect.top, 'width': w, 'height': h}) img = Image.frombytes('RGB', shot.size, shot.rgb) img.save(output_path) return img # Usage: capture_window('Visual Studio Code', 'vscode-capture.png')
Prerequisites: pip install mss pillow Limitation: Window must be visible (not behind other windows or minimized).
Node.js Playwright only — Python Playwright has no electron API. Captures via CDP (Chrome DevTools Protocol), not from the screen — works even while minimized.
javascriptconst { _electron: electron } = require('playwright'); const app = await electron.launch({ executablePath: 'C:\\Program Files\\Microsoft VS Code\\Code.exe', args: ['--new-window', '--disable-extensions', '--user-data-dir=' + tmpDir] }); const window = await app.firstWindow(); await window.waitForLoadState('domcontentloaded'); // Minimize immediately — captures still work via CDP await app.evaluate(({ BrowserWindow }) => { BrowserWindow.getAllWindows()[0].minimize(); }); await window.screenshot({ path: 'capture.png' }); // works while minimized! await app.close();
Critical: --user-data-dir=<temp> is required or VS Code hands off to the existing instance and the launched process exits immediately.
| Scenario | Tool | Notes | |---|---|---| | Web app (localhost) | Playwright | Proven, full DOM access | | Electron app (VS Code) | Playwright Electron (Node.js) | Works minimized via CDP | | Desktop app, visible window | mss + ctypes (find by title) | ~33ms per capture | | Desktop app, behind windows | Windows Graphics Capture API | Complex setup, Win10 1903+ | | Quick full-screen | mss | ~68ms |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +59 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.