Plugin

chatter.js

Google-Docs-style comments on any contenteditable. Select text, drop a comment, see a threaded sidebar. Persists in localStorage by default — no backend required.

npm install @goboldlyforward/chatter

Demo

Select text, drop a comment

Highlight any phrase in the editor below and click the 💬 chip that appears. Comments persist in this browser's localStorage; reload and they'll still be here.

The Hitchhiker's Guide to the Galaxy

Far out in the uncharted backwaters of the unfashionable end of the Western Spiral arm of the Galaxy lies a small unregarded yellow sun.

Orbiting this at a distance of roughly ninety-eight million miles is an utterly insignificant little blue-green planet whose ape-descended life forms are so amazingly primitive that they still think digital watches are a pretty neat idea.

Try it: select any text above. A 💬 chip will appear next to the selection — click it, write a comment, hit Cmd+Enter.

Then edit the doc: type before a highlight (it shifts), inside one (it grows and the sidebar quote updates), or delete one entirely (it orphans, with a red badge — paste it back to recover).

Install & usage

Drop it in

One stylesheet, one script, one attribute. No framework, no build step. Plugin auto-mounts on DOMContentLoaded.

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

<div id="doc1" data-chatter contenteditable="true">
  <p>Write here. Select any text and a 💬 chip will appear.</p>
</div>

<!-- sidebar gets injected as the next sibling -->

Each [data-chatter] element gets its own sidebar and its own storage bucket, keyed by the element's id (or data-chatter-key, or a generated CSS path as a fallback).

API

Methods & hooks

Chatter.init(element);     // mount one element
Chatter.initAll();         // re-scan for [data-chatter]
Chatter.clear(key);        // wipe comments for one element
Chatter.export();          // plain object: { [key]: Comment[] }
Chatter.import(data);      // bulk-load from export()'s shape
Chatter.author = 'Morgan'; // attribution for new comments

Backend sync via hooks

Chatter.onCommentAdd = async (comment) => {
  await fetch('/api/comments', {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify(comment),
  });
};

Chatter.onCommentUpdate = (comment) => { /* PATCH */ };
Chatter.onCommentDelete = (comment) => { /* DELETE */ };

Hooks fire after the local write, so a failed request doesn't lose the comment — the user still sees it locally and can retry via Chatter.export().

Comment shape

What gets stored

Each comment is a plain object — anchored by character offset into the element's plain text.

{
  id:        'chatter_o9ijosg',
  start:     179,                // char offset into element.textContent
  end:       189,
  text:      'yellow sun',       // the quoted text, for sidebar display
  body:      'Vivid color.',     // the comment itself
  author:    'You',
  timestamp: 1779587841986,
  resolved:  false,
  orphaned:  false,              // set true when the quoted text can't be located
}

Known limitations

v0.2 tradeoffs

  • Overlapping comments produce nested <mark> elements. Rendering is correct, but the deeper highlight wins visually.
  • Single-browser storage. Use the hooks for multi-user sync.
  • Recovery is exact-string only. If you rewrite a quoted phrase such that no verbatim copy of the original remains anywhere in the document, the comment orphans; delete it from the sidebar.