Block editor

Block editor is a Notion-like note editor: type / to add blocks and select text to format it. Use it for lesson notes and study pages.

  • Beta
  • Accessibility
  • Figma Structured Learning (Notes) 4480:138923
  • packages/ui/src/components/BlockEditor

Beta until design confirms the code-only parts (C52): heading 2 and 3 styles, the table toolbar, the image and maths dialogs, syntax colours and checked-item styling. The focus indicator (C5) is also open. Everything else on this page is implemented and tested.

Preview

A real editor with sample notes. Type / on an empty line for the block menu, select text for the formatting toolbar, click into the table for its toolbar, and click the formula to edit it. AI Summary, Run and image upload use demo stand-ins here.

html
<h1>My Notes</h1><p>A classic example is the Eames Lounge Chair. It is celebrated for its elegant looks, but every curve and material was chosen for comfort and durability. The form and function are so intertwined that they become inseparable. That synergy is often the hallmark of truly great design.</p><ul data-type="taskList"><li data-type="taskItem" data-checked="false"><p>Designed to be useful or practical rather than attractive.</p></li><li data-type="taskItem" data-checked="true"><p>Read the chapter on affordances.</p></li></ul><table><tbody><tr><th><p>Principle</p></th><th><p>Question to ask</p></th><th><p>Example</p></th></tr><tr><td><p>Affordance</p></td><td><p>What does it invite me to do?</p></td><td><p>A handle invites pulling</p></td></tr><tr><td><p>Feedback</p></td><td><p>Did my action work?</p></td><td><p>A button changes colour</p></td></tr></tbody></table><pre><code class="language-python">def golden_ratio(width):
    return width / 1.618</code></pre><div data-type="callout" data-emoji="💡"><div class="nu-block-callout__content">Form and function should be designed together, not one after the other.</div></div><div data-type="block-math" data-latex="\frac{a + b}{a} = \frac{a}{b} = \varphi"></div><p></p>

Installation

Availability

@nuton/ui and @nuton/tokens are private workspace packages and aren't published yet (D4, D12). Inside the Nuton monorepo, add them as workspace dependencies.

bash
pnpm add @nuton/ui @nuton/tokens @remixicon/react
tsx
import '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'

TipTap 3 and its table, task-list, image, maths and code extensions, KaTeX and lowlight install with @nuton/ui, pinned to exact versions (ADR 0007). Apps that never import BlockEditor don't bundle them. Maths renders as MathML, so no KaTeX stylesheet or fonts are needed.

Usage

tsx
import { BlockEditor } from '@nuton/ui'

export function LessonNotes({ saved, save }: { saved: string; save: (html: string) => void }) {
  return (
    <BlockEditor
      aria-label="Lesson notes"
      defaultValue={saved}
      onChange={save}
      onAiSummary={(editor) => summarise(editor.getText())}
      onImageUpload={(file) => uploadToStorage(file)} // resolves with the image URL
    />
  )
}
  • The value is HTML. The first block is the title (Figma “My Notes”); pass withTitle={false} for notes without one.
  • Connect the app's services: onAiSummary, onRunCode, onImageUpload and onFileUpload. Each feature only appears when its handler is set.
  • Save on change (debounced) rather than with a submit button, as people expect from note apps. Pass name when it lives in a form.
  • Sanitize stored HTML on the server before showing it to anyone else.
  • For a short formatted answer in a form, use Text editor instead.

Variants and sizes

The editor fills its column (Figma 698 wide). Content is made of blocks.

Blocks and the slash menu

Menu itemInsertsSource
AI SummaryCalls onAiSummary(editor). Shown only when you pass it.Figma
TextParagraphFigma
Header 1 · 2 · 3Headings (Label/X Large, Large, Medium)Figma
QuoteBlock quoteFigma
DividerHorizontal ruleFigma
Bullet List · Numbered ListLists; Tab and Shift+Tab nestFigma
ChecklistTask list with real checkboxesFigma
Table3 × 3 with a header row; a table toolbar adds and removes rows and columnsFigma (toolbar code-only)
Code blockLanguage menu, syntax colours, Copy, Delete; Run with onRunCodeFigma (colours code-only)
MathLaTeX, rendered as MathML; click to editFigma (dialog code-only)
Callout / HighlightTinted block with an emojiFigma
ImageFrom a web address, or uploaded with onImageUploadFigma (dialog code-only)

Typing after / filters the menu (“/check” → Checklist). Markdown shortcuts also work: # , ## , - , 1. , [ ] , > , ``` and ---.

Measurements

PartSpec
Blocks16 apart; body Paragraph/Small Text/Strong-950; title Label/X Large
Slash menu264 wide, radius 16, padding 8, gap 4, regular-shadow/medium; items 40 tall, radius 8, 20px icons
Selection toolbarFigma Rich Editor [1.1]: text style, size, B I U S, alignment, attach, link, image, more
Table1px Stroke/Soft-200 cells (Figma 128 wide), padding 10; header Bg/Weak-50
Code blockBg/Surface-800, radius 20; header 44 with a 2X-Small button group; body padding 10
CalloutState/Primary/Lighter, radius 20, padding 8 / 12, 24px emoji

Empty notes

A new note shows the title placeholder and Figma's prompt, “Start writing or type ‘/’ to see commands”.

States and interactions

Type /

Slash menuOpens at the cursor with a grey chip around the typed text. Arrow keys move, Enter or Tab inserts, Escape closes.

Select text

Selection toolbarAppears above a text selection. Not shown in code blocks, images or formulas.

Cursor in a table

Table toolbar (code-only)Add a row or column, delete a row, column or the table.

Read-only

Code-only additions (C52, pending design)

  • Figma shows the blocks but not their editing tools. Code adds the table toolbar, the image and maths dialogs, syntax colours, a checked-item style (struck through) and code output.
  • Header 2 and Header 3 use Label/Large and Label/Medium; Figma only shows Header 1 (Label/X Large).
  • Block drag handles aren't included: TipTap's drag-handle extension needs its collaboration packages.

Composition examples

Connecting your services

tsx
<BlockEditor
  aria-label="Lesson notes"
  defaultValue={note.html}
  onChange={debounce(saveNote, 800)}
  // AI Summary: read the text, insert the result as a callout
  onAiSummary={async (editor) => {
    const summary = await api.summarise(editor.getText())
    editor.chain().focus().insertContent({ type: 'callout', attrs: { emoji: '✨' }, content: [{ type: 'text', text: summary }] }).run()
  }}
  // Run: return output to show it under the code block
  onRunCode={(code, language) => api.runCode({ code, language })}
  onImageUpload={(file) => storage.upload(file).then((r) => r.url)}
  onFileUpload={(file) => storage.upload(file).then((r) => ({ url: r.url, name: file.name }))}
/>

API reference

Props

Block editor props
PropTypeDefaultDescription
defaultValuestring—Initial HTML.
valuestring—Controlled HTML. Replaces the document when it differs (not re-emitted).
onChange(html: string) => void—Called after every change; '' when empty.
withTitlebooleantrueThe first block is a title (Figma “My Notes”).
namestring—Adds a hidden input with the HTML for form submission.
disabledbooleanfrom FieldRead-only: no menus or toolbars.
aria-labelstring—Accessible name when there is no Field.
labelsPartial<BlockEditorLabels>BLOCK_EDITOR_LABELS (English)Every string: placeholders, menu items, toolbar and dialog labels.
onAiSummary(editor: Editor) => void—Shows “AI Summary” at the top of the slash menu.
onRunCode(code, language) => void | string | Promise<…>—Shows Run on code blocks. A returned string is shown as output.
onImageUpload(file: File) => Promise<string>—Adds “Upload from device” to the image dialog; resolve with the URL.
onFileUpload(file: File) => Promise<{ url, name }>—Shows the attach button in the selection toolbar; inserts a link.
defaultCodeLanguagestring'python'Language of new code blocks (Figma shows Python).
onEditorReady(editor: Editor) => void—Receives the TipTap editor.
refRef<HTMLDivElement>—Forwarded to the root.

Also exported: BLOCK_EDITOR_LABELS, BLOCK_EDITOR_LANGUAGES (code languages), BLOCK_EDITOR_FONT_SIZES and filterBlockCommands.

Accessibility

Keyboard

KeyBehaviour
/Opens the block menu at the cursor
↑ / ↓Move through the menu; Enter or Tab inserts; Escape closes
⌘/Ctrl / BBold (I, U, Shift+S for italic, underline, strikethrough)
Tab / Shift + TabIndent and outdent list items and checklist items
⌘/Ctrl / ZUndo (Shift+Z to redo)
Alt / → ←With a picture selected, makes it bigger or smaller (10% a step, never wider than the column). Dragging a corner does the same.

Screen readers

The editor is a multi-line textbox. While the block menu is open, the textbox points at the highlighted option (aria-activedescendant), so the choice is announced as you arrow. Checklist items are real checkboxes; formulas render as MathML.

Focus

Focus stays in the text while you use the slash menu. Toolbar and dialog choices return focus to the text.

Contrast and colour

Text Strong-950 (17.93:1), placeholder Sub-600 (6.69:1). The empty title placeholder uses Disabled-500 (4.23:1; Figma Disabled-300 was 1.53:1, C8). Code text is white on Surface-800 (15.13:1); the language label is Soft-400 (6.07:1).

Touch and pointer

Menu items are 40px tall; toolbar items 28px. Checkboxes are 16px inside a 20px row, the Figma size.

You must: give the editor a name (Field label or aria-label), ask for alt text when people add images, and translate labels outside English.

Figma mapping

FigmaCode
Container (423:20929) · Default / Variant2Empty-line prompt; the “/” chip and menu
Dropdown Items (423:20741)Slash menu: groups, items, AI Summary
Rich Editor [1.1] over the selectionSelection toolbar
My Notes (Label/X Large)Title block (withTitle)
Checkbox Label [1.1]Checklist items
Table, code, callout and image framesTable, code block, callout and image blocks

Guidelines and related components

Use Block editor for

  • Personal notes on a lesson
  • Study pages that mix text, tables, code and maths

Don't use Block editor for

  • A formatted answer inside a form (use Text editor)
  • Plain text (use a textarea)

Do and don't

<BlockEditor onImageUpload={upload} />
DoOnly connect services you support AI Summary, Run and upload appear only when their handler is set, so people never see a button that does nothing.
<BlockEditor onRunCode={() => {}} />
Don'tFake a feature with an empty handler A Run button that silently does nothing breaks trust.