collaborative-textarea
v1.0.1
Published
CRDT-powered <collaborative-textarea> custom element with remote cursors, persistence hooks, and TypeScript support.
Maintainers
Readme
Collaborative Textarea
CRDT-powered <collaborative-textarea> custom element with remote cursor overlays, persistence hooks, and first-class TypeScript types. Drop it into any web app, wire it to your transport of choice (WebRTC data channels work great), and get resilient collaborative editing without frameworks or build tools.
Why this component
- Uses a Text RGA CRDT (
simple-crdts) so edits merge deterministically and survive offline/latency. - Ships as a standard custom element; no React/Vue/Svelte runtime required.
- Remote cursor labels and colors are drawn in an overlay that matches your textarea’s typography.
- Built-in
onWritingPausedcallback andtoJSON()make persisting snapshots trivial. - Fully typed API surface, with zero dependencies beyond
simple-crdts.
Installation
npm install collaborative-textareaThe package is ESM-only ("type": "module"). Importing it registers the <collaborative-textarea> tag globally and also exports the class.
import { CollaborativeTextarea } from "collaborative-textarea";Quick start
Render the element, optionally hydrating from a saved snapshot and tagging the local user with a display name and color.
<collaborative-textarea
id="editor"
spellcheck="false"
style="width: 100%; height: 320px;"
></collaborative-textarea>
<script type="module">
import { CollaborativeTextarea } from "collaborative-textarea";
const saved = localStorage.getItem("collab-snapshot");
const rgaSnapshot = saved ? JSON.parse(saved) : undefined;
const editor = new CollaborativeTextarea(
"node-a",
{ name: "Ava", color: "#ec4899" },
[],
rgaSnapshot
);
editor.onWritingPaused = (snapshot) => {
localStorage.setItem("collab-snapshot", JSON.stringify(snapshot));
};
document.getElementById("editor").replaceWith(editor);
</script>Wiring up peers (e.g., WebRTC)
peerConnections expects objects with a send(operation) method. When using RTCDataChannel, wrap serialization yourself and call applyRemoteOp on inbound messages.
// Outbound: serialize before sending.
const channel = rtcPeerConnection.createDataChannel("collab");
const peer = {
send: (operation) => channel.send(JSON.stringify(operation)),
};
editor.addPeer(peer);
// Inbound: parse and feed into the CRDT.
channel.onmessage = ({ data }) => {
const operation = JSON.parse(data);
editor.applyRemoteOp(operation);
};You can also call insertCharAt, deleteCharAt, or applyRemoteOp directly if you bridge through another transport (WebSocket, BroadcastChannel, etc.).
API overview
Constructor
new CollaborativeTextarea(localNodeId?, userMeta?, peerConnections?, rgaSnapshot?)
localNodeId(string): unique id for this client. A UUID is generated if omitted.userMeta({ name?: string; color?: string; }): label and caret color shown in the overlay.peerConnections(CollaborativePeer[]): objects with asendmethod to broadcast operations.rgaSnapshot(TextRGAJSON): previously persisted CRDT state to hydrate from.
Key properties
spellcheck(boolean): mirrors the underlying textarea. Set via property orspellcheckattribute.onWritingPaused(callback | null): invoked ~1s after edits stop withtoJSON()snapshot.textarea(HTMLTextAreaElement): access the underlying textarea if you need to wire extra handlers.
Methods
addPeer(peer)/removePeer(peer): manage outbound peer targets.insertCharAt(index, char): local insert routed through the CRDT.deleteCharAt(index): local delete routed through the CRDT.applyRemoteOp(operation): apply an incoming insert/delete/cursor operation from a peer.toJSON(): CRDT snapshot suitable for persistence or hydration.
Styling
Style the host element as you would a normal textarea wrapper; the component mirrors your font, size, alignment, and padding onto the internal textarea so the cursor overlay lines up. Example:
collaborative-textarea {
display: block;
width: 100%;
height: 280px;
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 12px;
font: 400 16px/1.5 "Inter", system-ui, sans-serif;
}Persisting state
- Listen to
onWritingPausedfor a debounced snapshot callback, or calltoJSON()anytime. - Pass the stored snapshot back into the constructor to hydrate the CRDT with the correct ordering.
Notes and compatibility
- Requires browsers with Custom Elements and
ResizeObserversupport (falls back towindow.resizeif unavailable). - Ships unbundled ES modules; bring your own bundler or load directly in modern browsers.
- Dependency footprint:
simple-crdtsonly.
