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.