The knowledge base team writes articles inside a Retool app, and the website renders them. A plain text area loses headings and lists. An HTML field keeps them, and every downstream system then has to parse, clean, and sanitize markup before it can use the content. What the team needs is rich text stored as structured data.
Editor.js does that. It is a block-style editor whose output is JSON: one object per heading, paragraph, list, or quote. We packaged it as a Retool custom component, written in TypeScript, and published it on GitHub under the MIT license, free to use and extend.
What it does
Block editing. Headings (H1 to H4), paragraphs, ordered and unordered lists, quotes, code blocks, checklists, tables, delimiters, and warnings.
Inline formatting. Marker, underline, inline code, links, and footnotes.
Embeds. YouTube and CodePen.
JSON output. The
contentproperty holds the editor's state as a JSON string.Tool switches. Each tool can be turned on or off in the inspector.
Styling.
backgroundColor(default#f8fafc) andtextColor(default#000).
Install and deploy
You need Node.js 20 or later and an API access token with read and write scopes for Custom Component Libraries.
git clone https://github.com/StackdropCO/editorjs-retool-component.git
cd editorjs-retool-component
npm installLog in, create the component library in your Retool organization, and start development mode:
npx retool-ccl login
npx retool-ccl init
npx retool-ccl devWhen it is ready, deploy an immutable version:
npx retool-ccl deployThen open the Custom Component settings in your app and switch from dev to the version you just published. You may need to refresh the page before the new version appears.
Load and save content
Drag the Editor.js component onto the canvas. The content property works in both directions. Set it to load existing content, and read it to save what the user wrote. The component updates content as the user types.
In the examples below, the component is named editor1 and the content lives in a Postgres jsonb column. A query loads the selected article:
select body
from knowledge_articles
where id = {{ articlesTable.selectedRow.id }};Bind content to that result, as a string:
{{ JSON.stringify(getArticle.data.body[0]) }}A Save button runs the write query. Retool sends {{ }} values in a SQL query as prepared statement parameters, so the JSON arrives as one value:
update knowledge_articles
set body = {{ editor1.content }}::jsonb,
updated_at = now()
where id = {{ articlesTable.selectedRow.id }};For a new article, bind content to a temporary state variable instead, and insert on save.
What the data looks like
Each save stores a document like this one:
{
"time": 1635603431943,
"blocks": [
{
"id": "12345",
"type": "header",
"data": {
"text": "Editor.js",
"level": 2
}
},
{
"id": "67890",
"type": "paragraph",
"data": {
"text": "This is a paragraph block."
}
}
],
"version": "2.23.2"
}Because every block has a type and its own data, the content is easy to query, version, and render elsewhere. A website can map header blocks to its own heading styles, with no HTML cleanup in between.
Limit the tools
Every tool has a switch in the inspector: enableHeader, enableList, enableQuote, enableChecklist, enableCode, enableInlineCode, enableMarker, enableDelimiter, enableEmbed, enableTable, enableWarning, enableLink, enableUnderline, and enableFootnotes.
Turn on only what the content needs. A knowledge base might allow headings, paragraphs, lists, and links, and nothing else. A smaller tool set keeps the editor simple for authors and the stored content consistent.
Running it in production
Version the content schema. Editor.js writes its version into every document. If you change the tool set later, plan a migration for older block types.
Sanitize when you render. The JSON is structured, and embedded content can still carry markup. Sanitize on the way to HTML, especially for embeds.
Keep the JSON as the source. If a fast front end needs HTML, render and cache it. The JSON stays the version you edit.
Add a preview. A panel that renders the blocks lets authors see the result before publishing.
Convert where consumers need it. If another system expects HTML or markdown, add a converter from the block JSON. Libraries exist for both.
Guard against empty saves. Before the update runs, check that the parsed
contenthas at least one block, in the Save button's event handler. A blank editor should never overwrite an article:
// "Only run when" on the Save button's click handler
{{ JSON.parse(editor1.content || '{}').blocks?.length > 0 }}Pin a published version. Production apps should never point at the dev version of the library.
What is next
On the roadmap, not committed:
Drag-and-drop block reordering.
Custom block types configured from the inspector, such as a callout box or an embedded chart.
A full-screen editing mode.
Starter templates for blog posts and knowledge base articles.
A companion component that renders the JSON for preview.
Our view
We recommend storing rich text as Editor.js JSON when more than one system reads the content, because each reader can render blocks its own way. For a single internal note field, Retool's built-in Rich Text Editor is simpler, because it needs no library to deploy or maintain.
Issues and pull requests are welcome. For AI-assisted writing inside an app, see our AI rich text editor component. What is a custom Retool component covers when building one is worth it.