Plugin

pinpoint.js

Drop-in feedback widget. A floating button lets your users click anywhere on a page, capture a 400×200 thumbnail of the area, and attach a comment. Pins persist in sessionStorage by default — no backend required.

npm install @goboldlyforward/pinpoint

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.

[ hero image goes here ]

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.

No items yet.

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