draw on your live website. freehand feedback for ai agents.
@benjitaylor. he's one of the few people i genuinely look up to in this space. the way he thinks about problems, the craft he puts into agentation, the essays, it all made me want to build something.
agentation showed me that pointing at things beats describing them. markupkit started because i wanted to understand how that actually works under the hood. then i kept pulling the thread.
draw freehand on your live page. circle a bug. arrow between elements. strikethrough a typo. drag elements where you want them. resize. drop wireframe components. snap guides. the agent gets structured diffs with selectors and coordinates, not paragraphs.
this is a learning project. built in public, out of curiosity, because benji's work made me care about the problem.
npm i usemarkupkit -Dimport { Markup } from 'usemarkupkit'
function App() {
return (
<>
<YourApp />
{process.env.NODE_ENV === "development" && <Markup />}
</>
)
}zero config. drop it in, hit Ctrl+Shift+D, start drawing.
want layout mode too? add the prop:
<Markup layout />| mode | what it captures |
|---|---|
| draw | freehand strokes, classified into 7 shape types |
| text | exact selected string + element selector |
| click | any element, identified by content with computed context |
| multi | multiple elements grouped in one annotation |
| area | drag a rectangle, get every element inside it |
| pause | freezes all CSS animations and transitions globally |
every stroke gets classified with a confidence score. circle (enclose), arrow (relate two things), strikethrough (delete text), underline (emphasize), cross (remove), rectangle (select region), freehand (fallback). the classifier scores all seven and picks the highest above 0.35.
selectors built the way you'd actually search for them in code. id first, then data-testid, then meaningful classes, then nth-child as last resort. nothing brittle.
reads the fiber tree via __reactFiber$ keys, falls back to data-component and data-testid. component names appear in the output next to selectors.
reads _debugSource from the react fiber and data-source / data-file attributes. the agent gets the actual file path, not just a selector.
hover any element. padding renders as green zones, margin as amber zones, with pixel values labeled. computed live from getComputedStyle.
every text element gets its WCAG 2.1 contrast ratio calculated from color and backgroundColor. AA pass/fail, AAA pass/fail, both shown.
click the camera icon. annotation canvas downloads as PNG.
annotations survive page refresh. stored under markupkit_annotations. toggle on/off in settings.
annotations tell the agent this is wrong. layout mode tells the agent here's what i want instead.
select an element. drag it somewhere new. resize it. drop a wireframe component from the palette. every change is tracked with precise coordinates. hit copy, paste into claude code or cursor, the agent applies it in one pass.
| feature | what it does |
|---|---|
| drag to move | snaps to sibling edges and centers. delta captured as →12px, ↓8px |
| resize handles | 8 handles. drag edges or corners. 24px minimum enforced |
| alignment guides | blue dashed lines snap at 6px threshold. optional 8px grid |
| component palette | 9 wireframe primitives: section, card, heading, text, button, input, image, navbar, tabs |
| structured diffs | type, selector, from/to rects, pixel deltas per change |
| css suggestions | detailed level outputs transform and dimension CSS |
enable it:
<Markup layout />same annotations, four levels of detail. pick what your agent actually needs.
- compact — quick fixes, minimal context
- standard — selector + note, the default
- detailed — full computed styles, contrast, spacing, source file
- forensic — everything plus dom path, layout debug data, parent context
set it via prop or in the settings panel.
⚙ button in the toolbar. eight toggles, all wired live:
- output detail level
- spacing visualization
- contrast check
- react component detection
- source detection
- persist to localStorage
- clear on copy
- block interactions during annotation
| key | action |
|---|---|
Ctrl + Shift + D |
toggle on/off |
Esc |
cancel / close |
P |
pause/resume animations |
H |
hide/show markers |
C |
copy annotations |
X |
clear all |
all optional. works with zero config.
| prop | description |
|---|---|
enabled |
enable/disable (default: true) |
layout |
enable layout mode (default: false) |
color |
stroke color (default: "#171717") |
strokeWidth |
stroke width in px (default: 3) |
tool |
"draw" | "arrow" | "circle" | "eraser" |
detail |
"compact" | "standard" | "detailed" | "forensic" |
toolbar |
show toolbar (default: true) |
position |
toolbar corner (default: "bottom-right") |
root |
scope selector (default: "body") |
ignore |
tags to skip (default: []) |
shortcut |
toggle shortcut (default: "ctrl+shift+d") |
copyToClipboard |
write to clipboard (default: true) |
endpoint |
server URL for syncing annotations |
sessionId |
pre-existing session to rejoin |
onAnnotationAdd |
called when annotation created |
onAnnotationDelete |
called when annotation removed |
onAnnotationUpdate |
called when note edited |
onAnnotationsClear |
called when all cleared |
onCopy |
called when copy clicked, receives markdown |
onDraw |
called per-stroke with DrawEvent |
onSessionCreated |
called when a new session is created |
pass endpoint to sync annotations to your server. sessions get created on mount, annotations POSTed on change. pass sessionId to rejoin an existing session.
<Markup
endpoint="http://localhost:4747"
onSessionCreated={(id) => console.log("Session:", id)}
/>if you want to use the engine without the UI:
import {
detectShape, resolveShape, formatSession,
resolveClickedElement, resolveTextSelection,
resolveAreaSelection, getElementSpacingAtPoint,
contrastRatio, smoothPoints, strokeBounds
} from 'usemarkupkit'
// shape detection
detectShape(stroke) // → { type: 'circle', confidence: 0.87 }
// element resolution per mode
resolveClickedElement(x, y) // → Annotation from click
resolveTextSelection() // → Annotation from window.getSelection()
resolveAreaSelection(bounds) // → Annotation from rectangle
// spacing + contrast
getElementSpacingAtPoint(x, y) // → { spacing, rect }
contrastRatio('rgb(23,23,23)', 'rgb(255,255,255)') // → 16.0~10kb gzipped. zero dependencies. react 18+. typescript. MIT.
experimental software. built as a learning project, in public. see DISCLAIMER.md for the full version. use at your own risk. DYOR.
see SECURITY.md for reporting vulnerabilities.
see CONTRIBUTING.md for guidelines. issues, PRs, and ideas welcome.
MIT, see LICENSE.
made by @dragoon0x. inspired by @benjitaylor and agentation.