Annotation DSL
A typed JSON vocabulary that describes overlays without forcing you to assemble SVG fragments by hand. Available since:
@ingcreators/annot-annotator@0.2.0— canonical home@ingcreators/annot-playwright@0.2.0—annotateScreenshot()accepts it directly@ingcreators/annot-mcp@0.1.1— agent-facing tools accept it via MCPinputSchema
The same shape works from a Playwright test, an AI agent, or any
Node script that imports @ingcreators/annot-annotator. The
intent shorthand pulls colours from the
Annot design system so you don’t think in raw hex values.
Quick example
Section titled “Quick example”import { bboxAnnotationsToSvg, createAnnotator, type BboxAnnotation,} from "@ingcreators/annot-annotator";
const annotations: BboxAnnotation[] = [ { type: "rect", bbox: submitBtn, intent: "error" }, { type: "callout", at: { x: 50, y: 50 }, targetBbox: submitBtn, content: "Submit button disabled", },];
const png = createAnnotator().toPng({ originalDataUrl, annotationsSvg: bboxAnnotationsToSvg(annotations), width, height,});In a Playwright test, the same array goes straight into
annotateScreenshot():
const annotated = await annotator.annotateScreenshot(page, { annotations,});Annotation shapes
Section titled “Annotation shapes”Six shape variants — rect / circle / arrow / text /
callout / raw.
{ type: "rect", bbox: { x: 100, y: 50, width: 200, height: 80 }, intent: "error", // optional — see Intent shorthand stroke?: string, // raw colour override (CSS) strokeWidth?: number, // default 2 fill?: string, // default "none"}circle
Section titled “circle”{ type: "circle", center: { x: 150, y: 120 }, radius: 30, intent: "warning", stroke?, strokeWidth?, fill?,}{ type: "arrow", from: { x: 50, y: 50 }, to: { x: 200, y: 120 }, intent: "info", stroke?, strokeWidth?,}A self-contained <marker> definition is emitted per arrow so
multiple arrows on the same overlay don’t share marker ids.
{ type: "text", at: { x: 100, y: 50 }, content: "Failing here", intent: "error", color?: string, fontSize?: number, // default 14 anchor?: "start" | "middle" | "end", // default "start"}XML special characters (< > &) are escaped automatically.
callout
Section titled “callout”Composes a rect on the target + an arrow from the caption anchor to the nearest edge of the rect + the caption text. One DSL entry produces three drawn elements.
{ type: "callout", at: { x: 30, y: 30 }, // caption anchor targetBbox: { x: 200, y: 100, width: 80, height: 40 }, content: "Form validation broken", intent: "error", stroke?, color?,}numberedBadge
Section titled “numberedBadge”Numbered legend marker for “this is item N in a step-by-step overlay over a screenshot”. Composes a target rect outline + a filled intent-coloured circle at one corner + a bold white number inside the circle. The badge sits ON the target (not next to it) so the legend stays readable when the screenshot is scaled down in docs / slides.
{ type: "numberedBadge", bbox: { x: 200, y: 100, width: 80, height: 40 }, number: 1, placement?: "auto" | "topLeft" | "topRight" | "bottomLeft" | "bottomRight", badgeSize?: number, // diameter in px, default 40 imageWidth?: number, // used by placement: "auto" imageHeight?: number, intent?, stroke?, strokeWidth?, fill?, color?,}When placement is "auto" (the default) and both
imageWidth / imageHeight are supplied, the renderer picks the
corner of the target rect that’s furthest from the image edge so
the badge never clips. Without image dimensions, "auto" falls
back to "topRight".
Escape hatch for SVG you compose yourself:
{ type: "raw", svgFragment: `<g><circle cx="0" cy="0" r="5"/></g>` }Intent shorthand
Section titled “Intent shorthand”| Intent | Stroke | Fill (12% α) | Text |
|---|---|---|---|
info | #3b82f6 | rgba(59, 130, 246, 0.12) | #1e40af |
warning | #f59e0b | rgba(245, 158, 11, 0.12) | #92400e |
error (default) | #ef4444 | rgba(239, 68, 68, 0.12) | #991b1b |
success | #10b981 | rgba(16, 185, 129, 0.12) | #065f46 |
neutral | #6b7280 | rgba(107, 114, 128, 0.12) | #374151 |
If no intent is specified, error is used — the most common
case (a failing assertion, a broken element). Explicit stroke /
fill / color override the intent-derived defaults per-shape.
Locator-flavour DSL (MCP only)
Section titled “Locator-flavour DSL (MCP only)”@ingcreators/annot-mcp extends the DSL with a LocatorAnnotation
union — same shape unions, but positions accept Playwright locator
strings (button:has-text("Submit"), [data-testid="email"],
role=button[name="Sign in"], …) in addition to coordinates:
{ type: "rect", locator: "button:has-text('Submit')", intent: "error" }These get resolved to bboxes by the MCP server’s bundled
playwright-core browser pool before the SVG fragment is built.
See AI agents → Tools for the full schema.
Playwright-test users already hold a Page — call
await locator.boundingBox() and pass the result into a regular
{ type: "rect", bbox } instead.
JSON Schema
Section titled “JSON Schema”@ingcreators/annot-annotator ships JSON Schema literals for the
DSL — drop into a tool’s inputSchema $defs block when
exposing the DSL on a wire protocol:
import { BBOX_ANNOTATION_SCHEMA, BBOX_REDACT_REGION_SCHEMA, SHARED_DEFS,} from "@ingcreators/annot-annotator";SHARED_DEFS carries BBox / Point / Intent /
AnnotationStyle subschemas; the schema literals reference them
via #/$defs/BBox etc.
Marker id prefix change in 0.2.0
Section titled “Marker id prefix change in 0.2.0”arrowBetween (and any DSL arrow / callout rendered through
it) now uses annot-arrow-N for the inline marker id. The
0.1.x prefixes were annot-pw-arrow-N (playwright) and
annot-mcp-arrow-N (mcp). Snapshot-on-SVG tests in user code
may need to update.