Skip to content

Repository files navigation

pinpoint

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

Demo

goboldlyforward.github.io/pinpoint — open the trigger, click Drop a pin, then click anywhere on the page.

What it does

  • A round trigger button floats in a corner of your page (configurable: any of the four corners).
  • Click it: the settings panel opens and pin mode is on. Click anywhere on the page to drop a pin.
  • A 400×200 screenshot of the area around the click is captured (via html2canvas, lazy-loaded from a CDN on first use).
  • A composer popover opens at the pin location for a comment. Save with the button or Cmd/Ctrl + Enter.
  • Pins render as numbered teardrop markers on the page. Click a marker any time to read or delete it.
  • Feedback about a whole page rather than a point on it goes in the panel's Page note zone: paste a screenshot from the clipboard, drop an image file on it, or click to browse. Notes take a comment like a pin does, but carry no coordinates and drop no marker.
  • Click the trigger again (or the panel's X) to close the panel and exit pin mode.
  • Everything persists in sessionStorage (configurable to localStorage or in-memory).

No framework, no build step, no backend. Drop in the CSS + JS and you're done.

Install

npm install @goboldlyforward/pinpoint

Or grab the files directly:

<link rel="stylesheet" href="path/to/pinpoint.css">
<script src="path/to/pinpoint.js"></script>

Usage

<script>
  const pinpoint = new Pinpoint({
    position: 'bottom-left',
    storage:  'session',
    screenshot: true,
    keyboardTrigger: 'shift+meta+f',
  });
</script>

That's all the integration. The widget mounts itself when the DOM is ready.

Options

new Pinpoint({
  position:        'bottom-left',  // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
  storage:         'session',      // 'session' | 'local' | 'memory'
  storageKey:      'pinpoint:pins',
  screenshot:      true,
  screenshotWidth:  400,           // px; min 50
  screenshotHeight: 200,           // px; min 50
  html2canvasUrl:  'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js',
  keyboardTrigger: null,           // e.g. 'shift+meta+f'
  notes:           true,           // show the page-note drop zone
  noteMaxEdge:     1600,           // px cap on a note image's long edge; min 200
  noteQuality:     0.85,           // JPEG quality for note images; 0 < q <= 1
  noteMaxBytes:    2097152,        // byte ceiling for the encoded note image
  autoStart:       true,
  showMarkers:     true,
  onPinAdd:    (pin) => {},
  onPinDelete: (pin) => {},
  onPinClick:  (pin) => {},
});

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(data);
pinpoint.destroy();

Pin shape

{
  id:         'pin-l9wq4z-x4f2k1',
  kind:       'pin',                     // 'pin' | 'note'
  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,…', // 400x200 PNG, or null
  pageUrl:    'https://example.com/page',
  pageTitle:  'Example',
  createdAt:  '2026-05-23T14:02:11.000Z',
}

A note is the same shape with kind: 'note', an id prefixed note-, null for all four coordinates, and a thumbnail holding the whole screenshot rather than a crop. Pins saved before this field existed have no kind at all and are treated as 'pin'.

Page notes

Sometimes the feedback is about the screen, not a spot on it. With the panel open, the Page note zone takes an image three ways:

  • PasteCmd/Ctrl + V anywhere. On macOS Cmd + Ctrl + Shift + 4 puts a screenshot straight on the clipboard, so this is usually the fastest route.
  • Drop — drag an image file onto the zone.
  • Click — opens a file picker.

Whatever arrives is re-encoded before it becomes a note: the long edge is capped at noteMaxEdge and the image is written out as JPEG at noteQuality. If the result still exceeds noteMaxBytes, quality drops and then dimensions shrink until it fits, or the drop is refused with an inline message. Retina screenshots are routinely several megabytes; most backends will not take that.

Notes flow through onPinAdd exactly like pins, so a backend already receiving pins receives notes with no change beyond reading kind.

Shipping pins to a backend

Pinpoint is local-only by default. To send pins somewhere, wire up onPinAdd:

new Pinpoint({
  onPinAdd: async (pin) => {
    await fetch('/api/pins', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(pin),
    });
  },
});

The pin is already in storage by the time the hook fires, so a failed POST won't lose the pin — the user can retry from the panel's Export button.

Keyboard

  • Esc — cancel pin mode / close the composer / close the panel
  • Cmd/Ctrl + Enter — save the current comment
  • keyboardTrigger option — global hotkey to toggle pin mode (e.g. 'shift+meta+f')

Screenshots

Pinpoint lazy-loads html2canvas from jsDelivr the first time a pin is dropped. If the CDN is blocked, or if screenshot: false is set, pins save without a thumbnail. The screenshot ignores the Pinpoint widget itself so it doesn't appear in the capture.

Page notes need none of this — the image comes from the user's clipboard or disk, so html2canvas is never loaded for them and screenshot: false does not disable them. Set notes: false for that.

Requirements

HTML, CSS, and ~10KB of JavaScript. No framework, no build step. Uses pointer/keyboard events, sessionStorage/localStorage, and a lazy-loaded copy of html2canvas.

License

MIT — see LICENSE.

About

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

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages