Wings
Base typefaceOffset


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.

  1. First

  2. Second

  3. 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

HTMLLoading the editor…
JSONnabi-treeLoading the editor…
Install
npm install nabi-note
Code
import {
  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()))