A zero-dependency, hyper-lightweight visual feedback layer for the web.
Pindrop allows your team (or clients) to drop pins directly onto your live website or web app, leave threaded comments, and seamlessly collaborate—without ever leaving the browser.
Unlike heavy collaborative tools, Pindrop requires no backend, no accounts, and no dependencies. Out of the box, it runs entirely offline using your browser's local storage and generates shareable JSON files.
Need real-time sync for a larger team? Simply attach your own storage adapter to connect it to Firebase, Supabase, PostgreSQL, or any database you choose!
- Zero Dependencies: Pure vanilla JS and CSS. Plugs into React, Vue, Svelte, or vanilla HTML alike without bloating your bundle.
- Offline First: Out of the box, saves comments to
localStorageand generates.jsonexport blobs so you can share feedback securely over email, Slack, or Jira. - Bring Your Own Database: Want 'multiplayer' sync? Just pass a custom adapter object with
load()andsave()handlers to hook into your own API. - Smart Anchoring: Pins perfectly attach to layout components and automatically survive CSS class name mangling from modern compilers (Tailwind, CSS Modules).
- Automation Ready: Comes with a built-in Node CLI to convert your exported JSON files into Markdown reports. Also exposes a complete programmatic API for Playwright/AI bots to add and resolve feedback automatically.
- Dark Mode Support: Seamlessly respects your OS theme preference or can be forced into Dark/Light mode.
Install Pindrop via NPM: ```bash npm install pindrop.js ```
Drop Pindrop into the root of your frontend application:
```javascript import { Pindrop } from 'pindrop.js'; import 'pindrop.js/dist/style.css'; // Don't forget the CSS!
// Initialize Pindrop const pindrop = Pindrop.init({ storageKey: 'my-app-feedback', // LocalStorage prefix key theme: 'auto', // 'light' | 'dark' | 'auto' }); ```
You're done! Hit the c key to enter Comment Mode, click anywhere on the page, and leave a pin. Hit the v key to switch back to View Mode.
To upgrade Pindrop from offline-only to a fully synced collaborative tool, provide an adapter in the options.
```javascript const pindrop = Pindrop.init({ adapter: { load: async () => { // Fetch the comments from your backend API const res = await fetch('/api/comments'); return await res.json(); }, save: async (comments) => { // Save the entire array back to your database whenever a comment changes await fetch('/api/comments', { method: 'POST', body: JSON.stringify(comments) }); } } }); ```
Listen to Pindrop events to trigger custom actions (like sending an email notification when someone leaves a comment).
```javascript pindrop.on('comment:add', (comment) => { console.log('New feedback left by', comment.author); });
pindrop.on('comment:resolve', (comment) => { console.log('Issue resolved! Send a webhook to Jira.'); }); ```
Pindrop exposes a programmatic API so Playwright test suites or AI agents can directly manage the board without simulating mouse clicks.
```javascript // Discover semantic elements on the page before commenting const elements = pindrop.getCommentableElements(); // [{ selector: '#checkout-btn', label: 'Place order', rect: { x, y, width, height } }, ...]
// Drop a pin by CSS selector — returns the Comment or null if element not found const comment = pindrop.addComment({ selector: 'header > button.checkout', text: 'This button is failing the automated contrast test.', author: 'QA Bot', meta: { source: 'agent', model: 'gpt-4o' }, });
// Drop a pin by viewport coordinates (0–1) — useful when reasoning from a screenshot pindrop.addComment({ x: 0.5, y: 0.2, text: 'Header looks broken on mobile', author: 'Agent' });
// Reply to an existing comment — returns the Reply or null if (comment) { pindrop.addReply({ commentId: comment.id, text: 'Contrast ratio is 2.1:1, needs 4.5:1', author: 'QA Bot' }); }
// Resolve a comment — returns the updated Comment or null pindrop.resolveComment(comment.id, 'QA Bot'); ```
If your app reuses selectors across routes, tabs, or stateful screens, you can optionally attach scope metadata to each comment when it is created and decide whether that scope is currently active.
```javascript let activeScreen = 'checkout';
const pindrop = Pindrop.init({ getScope: (element) => { const screen = element.closest('[data-screen]')?.getAttribute('data-screen'); return screen ? { screen } : undefined; }, isScopeActive: (scope) => scope.screen === activeScreen, });
// Later, when your app changes screens/state: activeScreen = 'confirmation'; pindrop.refresh(); ```
Scoped comments only render when isScopeActive(scope) returns true. Unscoped legacy comments still render normally, and comments attached to hidden elements (e.g., in a hidden tab or modal) are automatically suppressed. Call pindrop.refresh() after your app switches routes or UI states so the overlay can re-evaluate visibility.
Convert offline JSON exports into readable Markdown reports straight from your terminal.
```bash
npx pindrop report ./my-comments.json
```
Outputs a beautifully formatted pindrop-report.md file!
Pull requests are welcome!
npm installnpm run devto start the live development server.npm run buildto compile the final library.
MIT License