It's Blok!
Blok is a headless, block-based rich text editor for the web. If you've used Notion, you know the feel: every paragraph, heading, image, or list is its own block that you can drag around, nest, and convert into something else.
The difference from a normal contenteditable setup is the data. A contenteditable field hands you one HTML blob and leaves you to parse it. Blok stores content as typed JSON blocks instead, so the output is the same whether you save it to a database, diff it, or render it on a server that never touches the DOM.
It's headless on purpose. Blok ships the editing engine and a set of tools; it does not impose a chrome or a theme. You wire it into your own UI.
What's in the box
| Feature | What you get |
|---|---|
| Block tools | Paragraph, heading, list, quote, callout, code, image, divider, table, toggle, and a column layout. Plus a Notion-style database block (rows are child blocks) and embed/bookmark blocks for pasted links. |
| Inline formatting | Bold, italic, underline, strikethrough, inline code, link, and a highlight marker. |
| Slash menu & markdown | Type / in an empty block to search and insert, or type markdown (#, -, 1., [], >) and it converts on space. |
| Drag and drop | Pointer-based reordering (not the flaky HTML5 drag API). Grab multiple blocks, hold Alt to duplicate while dragging, auto-scrolls near edges. Keyboard works too. |
| Undo/redo on Yjs | History is CRDT-backed: undo restores the caret, groups small edits, and batches atomically via blocks.transact(). |
| 68 locales, RTL | Reads the browser language, lazy-loads the matching locale, and lays out right-to-left scripts correctly. |
| Plugin system | Three extension points — block tools, inline tools, block tunes — with lifecycle hooks, paste handling, and conversion rules. Tools reach the editor through 18 API namespaces (blocks, caret, selection, …). |
| Smart paste | A handler chain keeps block structure intact on internal paste, strips Google Docs HTML noise, and lets tools claim specific file types or patterns. |
| Block conversion | Turn one block type into another from the inline toolbar or in code, one block or a whole selection at a time. |
| Read-only mode | Call readOnly.set(true) and the editor re-renders without editing affordances. |
| Accessibility | ARIA live announcements for drag and block ops, Notion-style vertical caret movement, semantic data attributes for tests. |
Installation
With a bundler (Vite, webpack, Rollup, etc.):
npm install @jackuait/blok
# or: yarn add @jackuait/blok / pnpm add @jackuait/blok// ESM
import Blok from '@jackuait/blok';
// CommonJS
const { Blok } = require('@jackuait/blok');The core package ships the engine but no tools, so you choose what to load. Import individual tools from @jackuait/blok/tools, or grab the batteries-included bundle:
import { Blok, defaultTools, defaultInlineTools } from '@jackuait/blok/full';
new Blok({
holder: 'editor',
tools: defaultTools,
inlineTools: defaultInlineTools,
});Other entry points
@jackuait/blok/react— React 18/19 adapter. The recommended entry point is<BlokEditor>, an all-in-one component that forwards a ref to the liveBlokinstance:const [data, setData] = useState(initialData); <BlokEditor tools={tools} data={data} onSave={setData} theme={theme} />;data+onSavemake<BlokEditor>a true controlled component.datais reactive: passing new content re-renders the editor in place (deep-equal–deduped, so identical content never clobbers the caret).onSaveis the output half: it fires — debounced — with the full serializedOutputDataon every content change, so you no longer pollref.current.save()by hand. WiringonSave={setData}is safe and caret-stable: the adapter records the editor's own emitted output as the content baseline, so echoing it back deep-equal–dedupes to a no-op (no re-render) while genuine externaldatachanges still render. (You can still forward a ref and callref.current.render(newData)for ad-hoc reloads, or use the lower-levelonChange(api, event)for mutation events.)Reactive props (
readOnly,theme,width,autofocus) sync without remounting. When structural config liketoolsneeds to change, pass adepsarray — the editor is destroyed and recreated whenever any dep value changes. Keep each value insidedepsreferentially stable: pass primitives oruseMemo-stable objects, since a dep value whose identity changes every render recreates the editor each time. (The individual values are compared, not the array wrapper, so a fresh[a, b]literal each render is fine whenaandbare stable; omittingdepscreates the editor once.)Don't wrap
<BlokEditor>instyled()or any HOC that reserves thethemeprop — styled-components claimsthemefor its ownThemeProvider, so it never reaches the editor and theme sync silently breaks. Render<BlokEditor>directly and style it throughclassName.For advanced control (e.g., rendering outside a single container), use
useBlok+BlokContentdirectly.@jackuait/blok/markdown—markdownToBlocks(md)to import Markdown (GFM, with optional math) as Blok data.@jackuait/blok/locales— locale data, if you'd rather load it yourself.
CDN (no bundler)
<script src="https://unpkg.com/@jackuait/blok/dist/blok.iife.js"></script>
<!-- or jsDelivr: https://cdn.jsdelivr.net/npm/@jackuait/blok/dist/blok.iife.js -->
<script>
const editor = new BlokEditor.Blok({ holder: 'editor' });
</script>The IIFE build puts everything under the BlokEditor global.
Documentation
Full docs live at blokeditor.com: API reference, an interactive demo, usage guides, and a migration guide if you're coming from Editor.js.
Community
There's a Telegram channel at t.me/that_ai_guy for updates and questions about Blok and related projects.
License & Attribution
Blok is licensed under the Apache License 2.0. See NOTICE for attribution.
Blok was forked from Editor.js by CodeX in November 2025 and reworked heavily since. The original Editor.js code remains CodeX under Apache-2.0; Blok-specific changes are JackUait, also under Apache-2.0.