noteloom
v0.5.0
Published
Zero-runtime-dependency, React-first block/rich-text editor — nestable content blocks, inline widgets, slash commands, undo/redo, clipboard, real-time collaborative editing (a custom CRDT over WebRTC), offline-first persistence (IndexedDB + PWA-ready), an
Downloads
611
Maintainers
Readme
noteloom
A React block editor with zero runtime dependencies. Nestable blocks, inline
widgets mid-sentence, slash commands, tables, undo/redo, clipboard — all built on
a small normalized store. The only things it needs from your app are react and
react-dom.
Docs & demo → · Playground → · Full guide
npm install noteloom react react-domimport { useEditor, NoteloomEditor } from 'noteloom';
function Editor() {
const editor = useEditor();
return <NoteloomEditor editor={editor} />;
}That's the whole thing — a working editor with every built-in block/inline type,
slash + @ + emoji menus, the formatting toolbar, keyboard shortcuts, clipboard,
find & replace, and the default theme, all wired up. No CSS import needed.
Pass a starting document (useEditor({ doc }) — the simple JSON format
or the internal shape, auto-detected), or history: false to drop undo/redo.
Highlights
- Inline widgets are first-class — a
selectdropdown, date picker, or@mentionchip sits in the middle of a sentence, not on its own line. - Fine-grained rendering — every block subscribes only to its own data; editing one paragraph in a 500-block doc repaints just that block.
- 13 built-in block types — paragraph, heading, list (bulleted/numbered/to-do/toggle),
table, multi-column layout, divider, callout, blockquote, code, toggle heading,
button, embed, canvas — plus atomic inline types (
select,date,checkbox, …). - Typed extension API —
defineBlock/defineInline/defineExtensionwith a stablectxfacade;npx noteloom new block <name>to scaffold one. - One canonical document format — self-contained JSON with a published
schema;
editor.toJSON(). - Opt-in heavy features — collaboration, persistence, comments, version history, voice typing each have their own import so they leave your bundle if unused.
- Retheme-able, RTL-aware, keyboard-operable, mobile/touch-first.
Composing the block set
useEditor() registers every built-in type. Pass extensions to control the
set — drop what you don't need, add the freehand-drawing canvas block from its
own (heavier) entry point, and register your own via defineBlock:
import { useEditor, NoteloomEditor, starterKit, defineBlock } from 'noteloom';
import { canvasBlockType } from 'noteloom/canvas';
import { RatingBlock } from './RatingBlock.jsx'; // a React component, receives { id }
// A whole custom block type — no text, no children, value lives in props.
// examples/02-custom-block/ is the runnable version of this.
const rating = defineBlock({
name: 'rating',
component: RatingBlock,
contentModel: 'void', // 'blocks' | 'runs' | 'void'
defaultProps: { stars: 0 },
toHTML: (block) => `<div data-stars="${block.props.stars}"></div>`,
slashCommand: {
label: 'Rating',
keywords: ['stars'],
run: (store, { blockId }) => {
/* erase "/rating", insert a { type: 'rating' } block after blockId */
},
},
});
function Editor() {
const editor = useEditor({
extensions: [
...starterKit({ exclude: ['canvas'] }), // every built-in except canvas…
canvasBlockType, // …then canvas back, explicitly, from noteloom/canvas
rating, // …plus your own
],
});
return <NoteloomEditor editor={editor} />;
}starterKit() on its own is the full default set (useEditor() with no
extensions is identical). defineExtension also carries behavior —
keymap, onBeforeInput, onPaste, setup(ctx) — and smartQuotes() /
autoPairBrackets() are ready-made ones. Full walkthrough:
guide → extension API.
Import map
The basic editor is one import. Heavy optional features have their own entry so a bundle that doesn't use them drops the code:
| Import | What's in it |
| ---------------------- | ------------------------------------------------------------------------------------ |
| noteloom | the editor, every built-in type, menus, clipboard, export, templates, find & replace |
| noteloom/theme | the default stylesheet (also noteloom/style.css) |
| noteloom/starter-kit | starterKit(), defineBlock, defineInline, registerExtensions |
| noteloom/collab | real-time collaboration (a custom CRDT over WebRTC) |
| noteloom/persistence | IndexedDB auto-save + PWA service-worker hook |
| noteloom/comments | comment threads + the built-in comment UI |
| noteloom/versions | automatic version history + <VersionHistory> |
| noteloom/voice | voice typing |
| noteloom/canvas | the freehand-drawing block |
Every name in a feature entry is still exported from noteloom too (deprecated;
see docs/migration.md).
[!TIP]
🛰️ Offline, serverless group editing
noteloom/collabis a custom block-tree CRDT over WebRTC — peers connect directly, so a group can co-edit one document over a LAN or an offline hotspot with nothing in the cloud. Bring any channel to exchange connection setup: aBroadcastChannel(same-machine tabs), a tiny WebSocket relay on the LAN (tools/lan-relay-server/), or Firebase / Supabase realtime.import { CollabSession } from 'noteloom/collab'; const session = new CollabSession({ history: editor.store, signaling }); session.connect(remotePeerId, { initiator: true }); // every edit now syncs to connected peers; incoming edits merge livePair it with
noteloom/persistenceand the doc survives every peer disconnecting. Experimental — guide → live collaboration.
Styling
No CSS import needed — the default theme injects on mount. Retheme via CSS
custom properties on :root:
:root {
--noteloom-accent: #16a34a;
--noteloom-radius-md: 4px;
--noteloom-font: 'Inter', sans-serif;
}Or theme="none" to style every .be-* class yourself. Dark mode follows
prefers-color-scheme (or data-theme="dark"). Details: guide → styling.
A future major will stop auto-injecting the theme — add
import 'noteloom/theme'now to keep it, ortheme="none"if you already style the editor.
Document format
const doc = editor.toJSON(); // { version: 1, blocks: [{ id, type, data, children? }] }
const restored = useEditor({ doc }); // loads it back — internal shape works tooSchema: docs/document.schema.json. The normalized
engine graph is available via editor.toJSON({ format: 'internal' }) when you
need it. HTML / Markdown / Word / plain-text export and a drop-in View source
button: guide → exporting.
Features (all in the full guide)
| Feature | |
| ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Custom dropdown / mention field types | static or API-backed, no component to write |
| Templates | reusable documents + insertable block snippets |
| Comments | thread on a range; built-in UI or bring your own |
| Version history | Google Docs-style automatic snapshots + diff |
| Offline persistence + PWA | IndexedDB auto-save, offline app shell |
| Live collaboration | multi-peer over WebRTC, bring-your-own signaling (experimental) |
| Voice typing | dictation + spoken commands via the browser's Speech API |
| Mobile / touch | bottom action bar, tap-friendly sheets |
| Find & replace | Ctrl/Cmd+F, match case / whole word |
| File & image uploads | data: URL by default, or wire uploadFile to S3/etc. |
| RTL / multi-language | automatic per-block direction |
| Accessibility | keyboard-operable menus, live-region announcements |
| The granular API | build the editor surface by hand |
Requirements
React 18.2+ or 19, and a modern browser (contentEditable + beforeinput;
IndexedDB for noteloom/persistence; WebRTC for noteloom/collab;
SpeechRecognition for noteloom/voice). SSR-safe — renders nothing on the
server and hydrates on mount.
Status
Pre-1.0 (0.4.x). Changes are additive only until a deliberate major —
existing code keeps working, deprecations get a full minor-version notice, and
the frozen list in docs/stability.md says exactly what
semver covers. noteloom/collab is experimental.
Contributing
Issues and PRs welcome — it's a small, opinionated project.
git clone https://github.com/vishwakarmanikhil/noteloom.git
cd noteloom && npm install
npm test # vitest (no build step needed)
npm run dev:quickstart # or dev:custom-block / dev:collab / dev:lan-collab / …Before a PR:
npm testandnpm run lint(CI runs the suite on Node 18/20/22; errors block, warnings don't).- Add/update tests —
test/mirrorssrc/. Touching rendering or export output? Refresh the golden snapshots (npx playwright test golden-document --update-snapshots) in the same commit. - Public API change? Update the entry file, its
.d.ts, and the frozen list intest/publicApi.test.js— the diff is the review signal. npm run changesetfor anything user-facing — it becomes the release note.- Keep the zero-runtime-dependency rule — nothing in
src/may add a runtimedependency.
Full detail — code layout, the framework-free-core boundary, sync-layer testing
advice — in CONTRIBUTING.md. Example apps and what each
teaches: examples/README.md.
License
MIT
