NABI NOTE
An open-source WYSIWYG editor
NABI NOTE is an open-source WYSIWYG editor whose formatting, alignment, tables, upload, and other features are separated from the core into independent modules called wings. You can choose only the wings you need to assemble an editor, and you can connect wings you build yourself. It is written in plain JavaScript, so it can be used from frameworks such as React or Vue. On pages without a build setup, you can start immediately with the CDN library. Its own document format, NABI TREE, can be converted to HTML even in Node.js environments without a browser DOM. Input documents are repaired through registered rules, and when HTML is created, text and attributes are escaped and link addresses are checked. However, custom wings and data received from the server must still be reviewed separately according to the application security policy. CSS variables and a rem-based layout let you adjust theme and size, and it also provides dark mode and typeface settings for multiple languages. Table sorting, local history, and AI-assisted document work all continue on the same document model.
Typeface
Language-specific fallback fonts are provided for sans serif (the default), serif, monospace, and cursive. If no font is available for the selected typeface, the browser's default font is used. The host page sets the editor's default typeface.
Below is each family shown in many languages.
산세리프 · ゴシック体 · 无衬线 · Serifenlos · Sans empattement · Sin serifa · Sem serifa · Без засечек · بلا زخارف · सैन्स सेरिफ़ · স্যান্স সেরিফ · سانس سیرف · Tanpa serif · بدون سریف · सॅन्स सेरिफ · Không chân · శాన్స్ సెరిఫ్ · Mara seref · Sans serif · Bila serif · சான்ஸ் செரிஃப் · ไร้เชิง · Senza grazie
세리프 · 明朝体 · 衬线 · Serif · Avec empattement · Con serifa · Com serifa · С засечками · بزخارف · सेरिफ़ · সেরিফ · سیرف · Berserif · سریف · सेरिफ · Có chân · సెరిఫ్ · Mai seref · Yenye serif · செரிஃப் · มีเชิง · Con grazie
고정폭 · 等幅 · 等宽 · Dicktengleich · Chasse fixe · Monoespaciada · Monoespaçada · Моноширинный · ثابت العرض · मोनोस्पेस · মনোস্পেস · یکساں چوڑائی · Lebar tetap · تکفاصله · Đơn cách · మోనోస్పేస్ · Tazara ɗaya · Eş aralıklı · Nafasi moja · ஒற்றையகலம் · ความกว้างคงที่ · Monospaziato · Monospace
필기체 · 筆記体 · 手写体 · Schreibschrift · Cursive · Cursiva · Рукописный · خط اليد · हस्तलिपि · হস্তলিপি · رواں خط · Tulisan tangan · دستنویس · हस्ताक्षर · Chữ viết tay · చేతిరాత · Rubutun hannu · El yazısı · Mwandiko · கையெழுத்து · ลายมือ · Corsivo
Text size
Extra small
Small
Large
Extra large
Heading
On an empty line, type # and then a space to turn it into a heading right there.
H1
H2
H3
H4
H5
H6
Bold · Italic · Underline · Strikethrough
Bold italic underline strikethrough can all be applied to the same character.
They can be stacked, too.
Superscript and subscript
The area is 3.5m2 and a footnote is added like this.1
Water is H2O.
Text color · Highlight
The palette is picked to stay readable in both light and dark mode.
Text color Green · Coral · Violet · Amber · Blue
Highlight Yellow · Green · Cyan · Pink · Purple · Orange
Link
Put in an address and it becomes a link.
You can use http:// and https:// URLs, as well as same-site paths that start with . or /. Addresses such as javascript: are not allowed.
For example, type https://nabi.saro.me and press space or enter — it converts on its own, as you see here.
Link opening
The document stores only the link address; the page that displays the document decides how links open.
Attachment link
Files other than images remain as attachment links like the one below.
Attachment links are saved in the document.
Alignment
Aligned left
Aligned center
Aligned right
Alignment cannot be applied to headings.
Lists
Bullet list
On an empty line, type - and press space — it becomes a bullet list right there.
This is a bullet item
Tab / Shift+Tab indent and outdent.
Numbered list
On an empty line, type 1. and press space to get a numbered list.
First
Second
Third
Checklist
On an empty line, type [ ] or [x] and press space to get a checklist.
This item is checked.
This one is not checked yet.
Table
Cells can hold multiple paragraphs and blocks, with background colors to distinguish their contents. Try editing the text and images in the table below.
Resources | Work notes |
|---|---|
An image and its description share one cell. | Flexible paragraphsPress Enter to start a new paragraph, or Shift+Enter to add a line break within the same paragraph. |
| Click for editing tipsCells can also contain details, quotes, code, and videos. Merging cells preserves their contents in order. |
Click inside a cell to show its background color and row/column tools. Select several cells to change their color and alignment together. To reposition the whole table, choose “Select table”, then use the alignment tools.
Tables with colors or multiple blocks are preserved as HTML even when saved as Markdown.
Cell background colors
All supported cell backgrounds are shown here. Click a cell to change its color, or choose “Default” in the property tools to clear its background.
Yellow yellow | Green green | Cyan cyan |
Pink pink | Purple purple | Orange orange |
Column sorting
Click Preview, then click the Stock and Price header cells in turn.
Model | Stock | Price | Weight |
|---|---|---|---|
NB-7 | 1,200 | 349 | 1.2 kg |
NB-9 | 20,000 | 99 | 0.9 kg |
NB-12 | 3,500 | 1,299 | 1.4 kg |
NB-80 | 900 | 8,900 | 2.1 kg |
NB-100 | TBD | 12,999 | 2.4 kg |
Price contains only numeric values, so the column is sorted numerically.
Stock contains both numbers and text, so the column is sorted as strings.
Divider
Type --- and press enter to turn it into a divider.
Image
The width goes from 30% to 100% and it can sit left, center or right.
Float region
At a glance
A small supporting region for text, images, and tables.
Placement | Right |
Maximum width | 40% |
Use a float region for extra explanations or small reference materials beside the main text. In a wide document, this text flows alongside the region. Click inside it to edit text and insert images or tables in the same editor.
Place the caret in the region to open its property tools. Switch between left and right placement, and choose a maximum width of 20, 30, 40, or 50% to suit your text. Pasted tables and images also stay inside the region.
You can also wrap selected paragraphs in a region. The unwrap button next to the width leaves its contents in the main document. Preview hides the editing outline, while narrow documents and print use a single column.
YouTube
Upload
Try dragging an image or a file onto the editor.
This demo does not send files to a real server. Connect an uploader in your application to store them on a server.
After opening an image picker or starting an upload, moving only the caret still inserts at the original position. Editing the document while waiting cancels insertion and shows a notice. Upload progress and results started in a cell also stay in that cell.
Quote
On an empty line, type > and press space to get a quote box.
It can run over several lines.
Code
On an empty line, type ``` and press space or enter to get a code box.
Write the language too, like ```java, and press space or enter to get a code box with that language applied.
import { createNabiWith, defaultWings } from 'nabi-note'
const { nabi } = createNabiWith(defaultWings)
const html = nabi.getHtml()Details
Details is made of a summary and a body.
You can choose whether it is saved folded or open.
Toolbar
Headings, text formatting, alignment, lists, insertion, and document tools are grouped together. Desktop shows the expanded tools; mobile offers quick tools and the full palette. Properties for the selected table or float appear below the toolbar.
Shortcuts
Press Shift twice quickly to choose tools with the keyboard. Navigate the expanded toolbar on desktop or the full palette on mobile. Use Tab to switch groups, arrow keys to choose a tool, and Enter to run it.
At the end of a block such as a quote, code block, or details, press Enter twice quickly to leave it. The default double-press interval is 180ms.
Autoformat
Example | Action key | Result |
|---|---|---|
# | Space | Heading |
- | Space | Bullet list |
1. | Space | Numbered list |
[ ] · [x] | Space | Checklist |
> | Space | Quote |
``` · ```ts | Space · Enter | Code box |
--- | Enter | Divider |
https://… | Space · Enter | Link |
Output functions
Function | Result |
|---|---|
getHtml() | HTML |
getJson() | JSON |
Works without a DOM
Converting to HTML from NABI TREE needs no DOM.
A server (Node.js) can read the stored NABI TREE and create HTML with the same rules.
Mobile friendly
Mobile UI — Arranges editing tools to fit the screen width.
Mobile keyboard adjustment — Adjusts the editing view height when the keyboard opens.
Flexible sizing — Uses rem units for the main layout to accommodate zooming in and out.
Multiple languages — You can set the locale and typeface to suit the language and text direction.
Customisation
Your own wing — if you need a feature, you can build it yourself and connect it.
Your own CSS — adjust the main colors, corners, and spacing with --nabi-* tokens to suit light and dark modes.
Open source — the source code is shared on GitHub, where we develop it together.
Read the docs → nabi.saro.me
npm install nabi-noteimport {
createLocale, createNabiWith, mountSurface, mountToolbar,
mountContextToolbar, mountHints, watchSettle, makeTranslator,
defaultWings, mountUpload, mountUploadView, browserFileStore,
mountFile, mountViewTools, browserHistoryStorage, mountLocalHistory,
openHistoryPanel,
} from 'nabi-note'
import { mountDiffWing } from 'nabi-note/diff'
import { mountOutline } from 'nabi-note/viewer'
// The box that asks — without it the core answers "no" to everything (plug in your own)
const ask = { message: (t: string) => alert(t), confirm: (t: string) => confirm(t) }
const selected = [
...defaultWings,
]
const locale = createLocale('en')
const { nabi, registry } = createNabiWith(selected, { ask, locale })
// Bring your own highlighter (Prism, highlight.js, Shiki, …) — the wing stays as it is
// and only the attach is swapped (`makeCodeAttach` ships from the same door)
// { ...codeWing, attach: makeCodeAttach({ highlight: (source, lang) => [{ text: source }] }) }
const root = document.querySelector('.nabi')!
const content = document.querySelector('.nabi-content')!
const view = mountUploadView({ nabi, surface: content, locale })
const upload = mountUpload({
nabi, root: content, locale,
// your upload goes here — report progress with task.onProgress(0–100)
uploader: async (task) => ({ uri: 'https://cdn.example/uploaded' }),
extensions: ['png', 'jpg', 'pdf'], maxFileSize: 10 * 1024 * 1024,
onStart: (tasks) => view.start(tasks),
onProgress: (id, percent) => view.progress(id, percent),
onSettle: () => view.settle(),
onDone: () => view.done(),
})
const file = mountFile({ nabi, registry, store: browserFileStore(document), locale, name: () => 'note' })
const history = mountLocalHistory({ nabi, storage: browserHistoryStorage(window) })
const diff = mountDiffWing({ nabi, registry, surface: content, locale })
// The locale also sets the direction — Arabic and Urdu run right to left
mountSurface({ nabi, registry, root: content, locale, fileSink: upload.take })
const trigger = document.createElement('button')
trigger.type = 'button'
trigger.textContent = makeTranslator(locale).t('outline')
trigger.style.cssText = 'position:fixed;right:1rem;bottom:1rem;z-index:1000'
root.append(trigger)
const outline = mountOutline(content, {
trigger, locale,
scrollContainer: () => root.classList.contains('is-fullscreen') ? root : window,
onChange: ({ items, activeIndex, available, open }) => {
trigger.hidden = !available
trigger.setAttribute("aria-expanded", String(open))
},
})
trigger.onclick = () => outline.toggle()
// Call outline.unmount() when removing the editor
const settle = watchSettle(document, { surface: content })
const shared = { nabi, registry, surface: content, settle, locale }
// A tool that needs a panel comes back to the host — without this the button is dead
const toolbar = mountToolbar({
...shared, root: document.querySelector('#toolbar')!,
file,
onFiles: upload.take,
onHost: (w) => {
if (w === 'diff') { diff.open(); return }
if (w !== 'localHistory') return
openHistoryPanel({
history, surface: content, locale, sessionId: history.sessionId,
render: (record) => createNabiWith(selected, { doc: JSON.parse(record.body) }).nabi.getHtml(),
})
},
})
const context = mountContextToolbar({ ...shared, root: document.querySelector('#context')! })
mountHints({ toolbar, context, root, surface: content })
// The preview and fullscreen buttons — they stand their own box at the end of the row
mountViewTools({ ...shared, root, container: document.querySelector('#toolbar')! })
// on every change — hook up your own code here
// nabi.onChange(() => user_callback(nabi.getHtml()))
// Call from your language selector. Content and editing history stay intact.
// locale.setLocale('ko')