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

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.

  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

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 paragraphs

Press Enter to start a new paragraph, or Shift+Enter to add a line break within the same paragraph.

  • Change cell backgrounds

  • Indent a list with Tab

  • Select several cells and apply the same color

Click for editing tips

Cells 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

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