Skip to content

Annotations — capabilities, live

Every demo on this page runs the real library inside its own frame, with a fetch interceptor playing the webhook — the event logs show genuine AFS v1.1 envelopes. Scenarios go from basic to advanced.

Every capture attaches a versioned, JSON-safe AnnotationForensics envelope alongside the flat AFS fields: stable selectors + full DOM path, geometry (document + viewport), grouped computed styles (layout, flex/grid, spacing, typography, appearance), ancestor and sibling context, accessibility (role, accessible name, ARIA, focusability), React component stack + dev-build source file:line when resolvable, and page context (URL, viewport, scroll, DPR). Report/prompt formatting is the application layer’s job — the library guarantees the data:

const forensics = window.__annotations.captureForensics("#cta");
// or read annotation.forensics off any created annotation

Selection parity — area, keyboard, freeze

Section titled “Selection parity — area, keyboard, freeze”

Four tools (element / text / multi / area drag-rectangle), Alt+arrow keyboard hierarchy navigation from the hovered element, deep shadow-root hit-testing, and a Pause motion toggle that freezes CSS animations, WAAPI animations, and playing videos so moving targets hold still. Each capture records its selectionMethod provenance in the forensics envelope:

The Edit mode turns an annotation into a structured design-edit request. Click an element to open a style panel (text/background color, font family, size, weight, line height, letter spacing, alignment, decoration) with live preview, or rewrite its copy inline via contentEditable. Saving creates a kind: "edit" annotation that keeps the usual forensic capture and optional comment, plus a versioned edits envelope precise enough to re-apply programmatically later — each operation records the property, the before value in both computed and authored form, the raw after value, and the target’s re-anchor keys:

{
"kind": "edit",
"edits": {
"version": 1,
"operations": [
{
"kind": "style",
"property": "color",
"before": { "computed": "rgb(24, 24, 27)", "authored": ".hero h2 { color: #18181b }" },
"after": { "value": "#7c3aed", "computed": "rgb(124, 58, 237)" },
"target": { "element": "h2", "selector": "h2.hero", "anchorSelector": "#hero-title", "fullPath": "body > main > h2" }
},
{ "kind": "text", "before": "Ship faster", "after": "Ship 10x faster", "target": { "element": "h2", "selector": "h2.hero" } }
]
}
}

Cancelling the panel — or deleting the annotation later — restores the page byte-for-byte (original inline styles and text). Edits flow through every existing channel unchanged: webhook annotation.created payloads, controlled setAnnotations/subscribeAnnotations round-trips, and uncontrolled persistence (a reload re-applies the saved edits to the page).

React-style state models. Controlled: the host owns the collection — seed via __init({ annotations }), replace wholesale with setAnnotations, observe with subscribeAnnotations, and pass overlay: false to skip the shadow DOM/picker/visuals entirely and render your own UI. Uncontrolled: pass persistence: "local" (or a custom PersistenceAdapter) and the overlay is self-contained — annotations survive reloads with no host backend:

// controlled — fully headless, host renders everything
window.__annotations.__init({ overlay: false });
const unsubscribe = window.__annotations.subscribeAnnotations(render);
window.__annotations.setAnnotations(hostCopy);
// uncontrolled — self-contained across reloads
window.__annotations.__init({ persistence: "local" });

The default composer always shows severity — blocking selected, one click to downgrade to suggestion. Hosts replace the whole field set without forking:

window.__annotations.__init({
composer: {
fields: [
{ key: "severity", label: "Severity", kind: "chips",
options: [{ value: "blocking" }, { value: "suggestion" }],
defaultValue: "blocking" },
{ key: "area", label: "Area", kind: "select",
options: [{ value: "copy" }, { value: "layout" }, { value: "behavior" }] },
],
},
});

Selectors break the moment a SPA re-renders. The resolver layers a verified-unique anchor selector → structural full path → context-scored candidates (stable classes, nearby text, bounding-box proximity, with a nearby-text contradiction guard) → explicit orphaned state. It never silently picks an identical-looking sibling.

Host-supplied buttons on the toolbar, composer, pin, or thread. Programmatic actions get the live re-anchored element; declarative actions are JSON-safe (they survive CDP injection) and emit action.requested for the host to handle:

window.__annotations.setActions([
{ id: "copy-html", label: "Copy HTML", placement: "pin",
run: (ctx) => navigator.clipboard.writeText(ctx.element?.outerHTML ?? "") },
{ id: "ask-agent", label: "Ask agent to fix", placement: "pin" }, // declarative
]);

Feedback is a conversation, not a mailbox. The agent replies in the pin’s thread, resolves annotations, opens threads, and shows a live presence cursor — the exact calls editor-web sends over CDP:

window.__annotations.replyToAnnotation(id, { role: "agent", content: "Fixed — please verify." });
window.__annotations.resolveAnnotation(id, "agent");
window.__annotations.focusAnnotation(id);
window.__annotations.setCursor({ id: "agent-1", kind: "agent", label: "Agent", x: 42, y: 300 });

The host owns canonical annotation state; the overlay is a derived view. Every (re)injection re-seeds via __init({ annotations }) — upserted by id, zero duplicate events — so live-view reloads never lose pins. Seeds can carry a scope so variant-A feedback never renders on variant B:

window.__annotations.__init({
annotations: savedAnnotations, // the host's durable copy
scope: { variantId: "variant-a" },
});

Opt-in, and the library never reads pixels itself: each new annotation emits an action.requested capture request (element requests include the clip box). The host captures — editor-web uses CDP Page.captureScreenshot — and attaches the validated result:

window.__annotations.__init({ screenshots: { onCreate: ["viewport", "element"] } });
// host side, after capturing:
window.__annotations.attachScreenshot(annotationId, {
kind: "element", format: "png", dataUrl,
});
  • Comparison matrix — how these capabilities stack up against Agentation, React Grab, and Cursor.
  • Editor-web integration — the ANNOTATIONS_IMPL flag, CDP injection, webhook mapping, testing.
  • Full API reference: packages/coframe-annotations/README.md.