screenshot-annotator
v0.5.0
Published
Capture annotated screenshots of any web UI. Annotations rendered as DOM overlays before Playwright captures. Arrows, highlights, callouts, multi-viewport, and replayable JSON specs so screenshots never go stale.
Maintainers
Readme
screenshot-annotator
Capture annotated screenshots of any web UI for documentation, tutorials, and product walkthroughs. Annotations (highlight boxes, numbered callouts, text labels, arrows) are injected into the page as real DOM elements before Playwright captures the screenshot, so they render at full resolution, scale with the page, and match your design system.
Built for teams whose user-facing docs, onboarding guides, or help-center articles keep going stale because the UI ships faster than someone remembers to retake the screenshots.

Why
Documentation screenshots usually mean:
- Take a screenshot manually
- Open Figma/Photoshop
- Add arrows and labels by hand
- Re-export
- Repeat every time the UI changes
This tool turns all of that into a single node command. Annotations are defined in code, anchored to selectors, and re-rendered automatically when the UI changes.
Install
npm (recommended)
npm install --save-dev screenshot-annotator playwright
npx playwright install chromiumClaude skill
npx skills add arjunkai/screenshot-annotatorThen in any conversation: "Take an annotated screenshot of localhost:5173 highlighting the login button". Claude will use the skill.
30-second example
import { chromium } from 'playwright';
import { annotate } from 'screenshot-annotator';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://opbindr.com');
await annotate(page, [
{
type: 'arrow',
fromTarget: page.getByRole('heading', { name: /collection/i }),
toTarget: page.getByRole('button', { name: /create your first binder/i }),
},
{
type: 'label',
target: page.getByRole('button', { name: /create your first binder/i }),
text: 'Click to begin',
position: 'right',
},
]);
await page.screenshot({ path: 'home.png' });
await browser.close();That's it. The arrow and label render at the correct positions on the live page.
Default color is red (#ef4444), the docs convention for "look here." Pass color: on any annotation to match your brand:
{ type: 'arrow', fromTarget: ..., toTarget: ..., color: '#3b82f6' } // blue
{ type: 'label', target: ..., text: 'New', color: '#10b981' } // greenThe killer feature: spec replay
Screenshots in docs go stale because the UI changes faster than people remember to retake them. This tool fixes that by saving the intent of each screenshot as a JSON sidecar:
{
"url": "https://opbindr.com",
"viewports": [
{ "name": "desktop", "width": 1440, "height": 900 },
{ "name": "mobile", "width": 390, "height": 844 }
],
"setup": [
{ "action": "waitForSelector", "selector": "h1" }
],
"annotations": [
{
"type": "label",
"selector": "role=button[name=/create your first binder/i]",
"text": "Click to begin",
"position": "right",
"color": "#ef4444"
}
]
}When the UI changes, re-render every screenshot in your docs with one command:
npx screenshot-annotator replay public/guideFor each *.spec.json it finds, you get a fresh .png next to it (one per viewport: feature.desktop.png, feature.mobile.png).
Want to point at staging or a feature branch instead of prod? Override the origin with SCREENSHOT_URL:
SCREENSHOT_URL=https://staging.myapp.com npx screenshot-annotator replay public/guideSetup helpers for real-world pages
Scripts that run against real apps keep hitting the same four gotchas. The package exports helpers for each, so you don't have to debug them yourself:
hoverByMouse(page, locator)bypasses Playwright's actionability stall on re-rendering elements (virtualized lists, live data, CSS transitions)waitForImagesLoaded(page, opts?)waits until ≥90% of visible images have decoded, so your screenshots aren't half-loaded gridsraceVisible(page, locatorMap, opts?)handles branching landing pages where a first-time user and a returning user see different buttons- Plus: don't use
waitUntil: 'networkidle'on a dev server. HMR keeps WebSockets open andpage.gototimes out. Use'domcontentloaded'instead.
Full explanation of each gotcha and how to use the helpers is in SKILL.md.
Annotation primitives
| Type | What it does | Use when |
|---|---|---|
| highlight | Colored rectangle with darkened backdrop around a target | "This is the thing I'm talking about" |
| callout | Numbered circle at a corner of a target (1, 2, 3…) | Step-by-step sequences referenced from text |
| label | Text pill anchored to a target with position: 'right' \| 'left' \| 'above' \| 'below' | Inline explanations |
| step | Numbered pill with text fused inline ([1] Click here) | Tutorial steps that pair a number with a description |
| arrow | SVG arrow between two locators or coordinates | Connecting two elements visually |
Each annotation accepts either Playwright Locators (page.getByRole(...), page.getByText(...)) when used in a script, or selector strings (role=button[name="Save"], text="Filters") when used in a spec.
Capturing interactive states
Hover effects, dropdowns, focus rings, and tooltips only appear when the user interacts with the page. Specs support setup actions to trigger them:
{
"setup": [
{ "action": "hover", "selector": ".user-avatar" },
{ "action": "focus", "selector": "input[name='search']" },
{ "action": "type", "selector": "input[name='search']", "text": "luffy" },
{ "action": "press", "key": "Enter" },
{ "action": "scroll", "selector": ".footer" }
]
}Full list: click, hover, focus, fill, type, press, scroll, waitForSelector, waitForTimeout.
Quick start without writing any code
npx screenshot-annotator example # writes a starter example.spec.json
npx screenshot-annotator replay . # produces example.png + example.mobile.pngCLI reference
npx screenshot-annotator replay <dir> # re-render every spec in <dir>
npx screenshot-annotator example # write a starter spec in cwd
npx screenshot-annotator --helpEnv vars:
SCREENSHOT_URL: override the origin of every spec (point at staging/dev)
Programmatic API
import {
annotate, clearAnnotations,
saveSpec, loadSpec, replaySpec,
hoverByMouse, waitForImagesLoaded, raceVisible,
themes,
} from 'screenshot-annotator';themes.bright and themes.muted are color palettes you can spread into annotations to stop repeating hex literals. Each is { primary, accent, success, warning, info, text }.
See examples/example-script.js for a complete worked example that captures + saves spec + supports multi-viewport.
Requirements
- Node 18+
- Playwright (
npm install -D playwright && npx playwright install chromium) - A web UI to screenshot (local dev server or production URL)
License
MIT
