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
Sans serif (the default), serif, monospace and cursive — each family stacks fonts per script, so whatever language you write in keeps that family's shape, and a script with no handwriting face in that family falls back to the browser's own font. The host decides the default typeface.
Below is each family shown in many languages.
Sans serif · 산세리프 · ゴシック体 · 无衬线 · Serifenlos · Sans empattement · Sin serifa · Sem serifa · Без засечек · بلا زخارف · सैन्स सेरिफ़ · স্যান্স সেরিফ · سانس سیرف · Tanpa serif · بدون سریف · सॅन्स सेरिफ · Không chân · శాన్స్ సెరిఫ్ · Mara seref · Bila serif · சான்ஸ் செரிஃப் · ไร้เชิง · Senza grazie
Serif · 세리프 · 明朝体 · 衬线 · Avec empattement · Con serifa · Com serifa · С засечками · بزخارف · सेरिफ़ · সেরিফ · سیرف · Berserif · سریف · सेरिफ · Có chân · సెరిఫ్ · Mai seref · Yenye serif · செரிஃப் · มีเชิง · Con grazie
Monospace · 고정폭 · 等幅 · 等宽 · Dicktengleich · Chasse fixe · Monoespaciada · Monoespaçada · Моноширинный · ثابت العرض · मोनोस्पेस · মনোস্পেস · یکساں چوڑائی · Lebar tetap · تکفاصله · Đơn cách · మోనోస్పేస్ · Tazara ɗaya · Eş aralıklı · Nafasi moja · ஒற்றையகலம் · ความกว้างคงที่ · Monospaziato
Cursive · 필기체 · 筆記体 · 手写体 · Schreibschrift · 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 — an example.
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
Uploading anything other than an image leaves a file-shaped link like the one below.
Attachment is how it stays.
Alignment
Aligned left
Aligned center
Aligned right
Headings can be aligned as well.
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
Click the table button in the toolbar to make one, then add, delete and merge rows and columns.
Column sorting
Preview opens first; 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 is all numbers, so it sorts numerically.
Stock sorts as text because of the letters in the last cell.
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.
YouTube
Upload
Try dragging an image or a file onto the editor.
The upload used here is a mock-up; a setting connects it to your server.
If an upload fails, the image or file is removed from the editor.
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.
Shortcuts
Press Shift twice quickly to show each feature shortcut in the toolbar.
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 JSON needs no DOM.
A server (Node.js) can read the stored NABI TREE and create HTML with the same rules.
Mobile friendly
Mobile UI — a responsive layout carries the mobile UI.
Mobile keyboard inset — when the keyboard opens, its height is accounted for.
Fluid sizing — every size is written in rem.
Many languages — it supports multiple languages.
Customisation
Your own wing — if you need a feature, build it yourself and register it.
Your own CSS — colors, corners and spacing are all defined as --nabi-*, so dark, light and anything else is yours to set.
Open source — it is open source on GitHub.
Read the docs → nabi.saro.me
npm install nabi-noteimport {
createNabiWith, mountSurface, mountToolbar, mountContextToolbar,
mountHints, watchSettle, defaultWings, mountUpload, mountUploadView,
browserFileStore, mountFile, parseNodes, mountViewTools,
browserHistoryStorage, mountLocalHistory, openHistoryPanel,
} from 'nabi-note'
import { mountDiffWing } from 'nabi-note/diff'
// 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 { nabi, registry } = createNabiWith(selected, { ask })
// 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 })
const upload = mountUpload({
nabi, root: content,
// 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), parse: parseNodes, locale: 'en', name: () => 'note' })
const history = mountLocalHistory({ nabi, storage: browserHistoryStorage(window) })
const diff = mountDiffWing({ nabi, registry, surface: content, locale: 'en' })
// The locale also sets the direction — Arabic and Urdu run right to left
mountSurface({ nabi, registry, root: content, locale: 'en', fileSink: upload.take })
const settle = watchSettle(document, { surface: content })
const shared = { nabi, registry, surface: content, settle, locale: 'en' }
// 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: 'en', 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()))