Demo
Click the pin, drop feedback anywhere
The trigger button is live in the bottom-left corner of this page. Click it: the panel opens and pin mode is on — click anywhere on the page to drop a pin. Click the trigger again to exit. Pins persist in this tab until you close it.
Login form
A sample form to leave feedback on. Try dropping a pin on the misaligned button.
Hero image
Pretend this is a marketing hero. Pin the copy, the placeholder, or the spacing.
Pricing card
$19 / month for the basic plan. Want to flag a confusing label or a typo? Drop a pin.
Empty state
Empty states are notoriously bad. Pin the parts that would confuse a first-time user.
Tip: open the trigger button and try the position picker, the pin list, and the export button. Press Esc to cancel pin mode.
Install & usage
Drop it in
One stylesheet, one script. No framework, no build step. Screenshots use html2canvas, lazy-loaded from a CDN the first time you enter pin mode.
<link rel="stylesheet" href="path/to/pinpoint.css">
<script src="path/to/pinpoint.js"></script>
<script>
const pinpoint = new Pinpoint({
position: 'bottom-left', // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
storage: 'session', // 'session' | 'local' | 'memory'
screenshot: true,
keyboardTrigger: 'shift+meta+f',
});
</script>
That's all the integration. The widget auto-mounts on DOMContentLoaded. Pin clicks are intercepted only while the user is in pin mode; the rest of the time the trigger button is the only Pinpoint UI on the page.
API
Options
new Pinpoint({
position: 'bottom-left', // initial corner
storage: 'session', // 'session' | 'local' | 'memory'
storageKey: 'pinpoint:pins', // where pins live in storage
screenshot: true, // capture 200x200 thumbnail
screenshotWidth: 400, // px (min 50)
screenshotHeight: 200,
html2canvasUrl: 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js',
keyboardTrigger: 'shift+meta+f', // optional global hotkey
autoStart: true, // mount on construction
showMarkers: true, // render persisted pin dots
onPinAdd: (pin) => {}, // hook: pin saved
onPinDelete: (pin) => {}, // hook: pin removed
onPinClick: (pin) => {}, // hook: marker opened
});
Methods
pinpoint.enable(); // enter pin mode
pinpoint.disable(); // leave pin mode
pinpoint.toggle();
pinpoint.setPosition('top-right');
pinpoint.getPins(); // returns shallow copies
pinpoint.deletePin(id);
pinpoint.clear();
pinpoint.exportJSON(); // returns JSON string
pinpoint.exportFile(); // downloads pinpoint-pins.json
pinpoint.importJSON(json); // bulk-load
pinpoint.destroy(); // unmount + unbind
Pin shape
What gets stored
Each pin is a plain object — easy to ship to a backend later if you wire up onPinAdd.
{
id: 'pin-l9wq4z-x4f2k1',
x: 452, // px from document left
y: 1280, // px from document top
xPercent: 0.31, // x as fraction of document width
yPercent: 0.78, // y as fraction of document height
viewport: { width: 1440, height: 900 },
document: { width: 1440, height: 1640 },
body: 'Misaligned button',
thumbnail: 'data:image/png;base64,…', // 200x200 PNG, or null
pageUrl: 'https://example.com/page',
pageTitle: 'Example',
createdAt: '2026-05-23T14:02:11.000Z',
}