HellDots

Quick start

From an empty project to a working comment overlay, then out to your own backend.

Install

npm install helldots

Mount the overlay

Call it once, from the browser, as early as you like — it defers its own DOM work until the document is ready.

app.js
import { createCommentOverlay } from 'helldots';

createCommentOverlay({
  user: { name: 'Ana' },
  persistence: 'localStorage',
});

Leave a comment

A toolbar appears at the bottom of the page.

  • Alt+C (Option+C on macOS) toggles comment mode.
  • Click anywhere to anchor a comment to that element.
  • Drag to select a region and attach a full-resolution crop of it.
  • The toolbar's inbox lists every comment, filters them, and shows the metrics dashboard.

That is the whole of it. Comments survive a reload, re-anchor themselves when the page changes, and stay in the visitor's browser.

Try it here

This site is running exactly that. Open the Playground and drive the widget through its public API.

Wiring it to your own backend

persistence: "localStorage" is a convenience, not the design. Drop it and subscribe instead — every comment is plain JSON.

comments.js
import { createCommentOverlay } from 'helldots';

const overlay = createCommentOverlay({
  user: { name: currentUser.name, id: currentUser.id },

  onCommentCreated: (comment) => api.post('/comments', comment),
  onReplyAdded: (comment, reply) => api.post(`/comments/${comment.id}/replies`, reply),
  onCommentStatusChanged: (comment) => api.patch(`/comments/${comment.id}`, comment),
  onCommentUpdated: (comment) => api.patch(`/comments/${comment.id}`, comment),
  onCommentDeleted: (id) => api.delete(`/comments/${id}`),

  // The safe place to load: the widget has mounted and the counts are real.
  onReady: async (o) => {
    const { orphaned } = o.loadComments(await api.get('/comments'));
    if (orphaned) console.info(`${orphaned} comments lost their element`);
  },
});

Ten separate callbacks is one way. The other is a single stream, which is usually what you want when everything goes to one endpoint:

createCommentOverlay({
  onChange: (event) => api.post('/helldots-events', event),
});

ChangeEvent is a discriminated union — switch on event.type and TypeScript narrows the payload for you. Both routes carry the same events at the same moments; subscribe either way, or both.

Load from onReady

loadComments() is safe to call at any point — called before the widget has mounted, the data is held and applied at mount. But the counts it returns come back as zeroes, because nothing has been resolved against the DOM yet. Use onReady when those numbers matter.

A React component

Comments.jsx
import { useEffect } from 'react';
import { createCommentOverlay } from 'helldots';

export function Comments({ user }) {
  useEffect(() => {
    const overlay = createCommentOverlay({ user, persistence: 'localStorage' });
    return () => overlay.cleanup();
  }, [user]);

  return null;
}

cleanup() removes the widget entirely, which is what makes this safe under React's development double-mount. For a client-side router you also want notifyNavigation() — see frameworks.

Next steps

On this page