Skip to content

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.0annotateScreenshot() accepts it directly
  • @ingcreators/annot-mcp@0.1.1 — agent-facing tools accept it via MCP inputSchema

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.

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,
});

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"
}
{
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.

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?,
}

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>` }
IntentStrokeFill (12% α)Text
info#3b82f6rgba(59, 130, 246, 0.12)#1e40af
warning#f59e0brgba(245, 158, 11, 0.12)#92400e
error (default)#ef4444rgba(239, 68, 68, 0.12)#991b1b
success#10b981rgba(16, 185, 129, 0.12)#065f46
neutral#6b7280rgba(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.

@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.

@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.

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.