Nasaq
Components

RichTextEditor

Tiptap-based rich text editor with a Nasaq toolbar, per-paragraph auto direction, HTML or JSON output and read-only mode.

PreviewOpen ↗

Code

import { RichTextEditor } from "@fadymondy/nasaq/web";import { useState } from "react";export function Notes() {  const [html, setHtml] = useState("<p>Hello</p>");  return <RichTextEditor aria-label="Notes" value={html} onValueChange={setHtml} />;}

Editors · beta

Live examples and controls: RichTextEditor in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/rich-text-editor.json

A WYSIWYG editor on Tiptap v3 (ProseMirror). The toolbar offers bold, italic, underline, strike, inline code, headings 1-3, bulleted and numbered lists, quote, link, undo and redo, each with a tooltip that shows its keyboard shortcut. Every block gets dir="auto", so an Arabic paragraph and an English paragraph in the same document each align from their own side.

Bundle cost

Tiptap plus ProseMirror is roughly 150-200 KB minified (about 50-60 KB gzip) before Nasaq code. The component is self-contained (no other Nasaq component imports it) and the packages are ES modules with sideEffects limited to CSS, so an app that never renders it does not ship it. Load it lazily where it appears:

import { lazy, Suspense } from "react";

const RichTextEditor = lazy(() => import("@nasaq/web").then((m) => ({ default: m.RichTextEditor })));

export function Body(props: { value: string; onChange: (v: string) => void }) {
  return (
    <Suspense fallback={<div className="h-40 rounded-control border border-border" />}>
      <RichTextEditor aria-label="Body" value={props.value} onValueChange={props.onChange} />
    </Suspense>
  );
}

When to use

  • Comments, descriptions, notes and articles that need formatting.
  • Read-only rendering of content that was authored here (readOnly).

When not to use

  • Plain multi-line text: use Textarea. Mentions in plain text: mention-textarea.
  • Displaying Markdown: use markdown.
  • Code editing: this is not a code editor.

Import

import { RichTextEditor } from "@fadymondy/nasaq/web";

Quick start

import { RichTextEditor } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Notes() {
  const [html, setHtml] = useState("<p>Hello</p>");
  return <RichTextEditor aria-label="Notes" value={html} onValueChange={setHtml} />;
}

Anatomy

RichTextEditor                  data-slot="rich-text-editor"
  toolbar                       data-slot="rich-text-editor-toolbar" (role="toolbar")
    ToggleGroup (marks) / ToggleGroup (blocks) / link Popover / undo / redo
  content                       data-slot="rich-text-editor-content" (role="textbox", aria-multiline)

API

PropTypeDefaultDescription
format"html" | "json""html"Type of value, defaultValue and onValueChange.
valuestring | JSONContentControlled document. Changes from outside replace the content without echoing.
defaultValuestring | JSONContent""Uncontrolled initial document.
onValueChange(value: string) => void (JSONContent for format="json")Fires on every edit.
placeholderstringlocalised "Write something…"Shown while empty.
readOnlybooleanfalseNo toolbar, no editing, no border.
toolbarRichTextToolbarItem[]allItems and their order (bold italic underline strike code h1 h2 h3 bulletList orderedList blockquote link undo redo). [] hides the toolbar.
minHeightstring"10rem"Minimum height of the writing area.
aria-labelledbystringid of the label naming the editor. Without it the editor is named "Rich text editor".
onBlur() => voidFocus left the editor.
classNamestringClasses for the outer frame.

Install @tiptap/pm alongside (peer of @tiptap/react). Also exported: AutoDirection (the Tiptap extension that adds dir), isSafeLink(url) (http(s), mailto, tel, relative and # links only) and the type RichTextJson.

Examples

Field label and Arabic:

import { Field, FieldLabel, RichTextEditor } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function ArabicBody() {
  const [html, setHtml] = useState("<p>مرحبًا</p>");
  return (
    <div lang="ar" dir="rtl">
      <Field>
        <FieldLabel id="body-label">المحتوى</FieldLabel>
        <RichTextEditor aria-labelledby="body-label" value={html} onValueChange={setHtml} />
      </Field>
    </div>
  );
}

JSON output:

import { RichTextEditor, type RichTextJson } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function JsonNotes() {
  const [doc, setDoc] = useState<RichTextJson>({ type: "doc", content: [{ type: "paragraph" }] });
  return <RichTextEditor format="json" aria-label="Notes" value={doc} onValueChange={setDoc} />;
}

Accessibility

KeyAction
Ctrl/Cmd+B, I, UBold, italic, underline.
Ctrl/Cmd+Shift+SStrikethrough.
Ctrl/Cmd+EInline code.
Ctrl/Cmd+Alt+1-3Headings.
Ctrl/Cmd+Shift+7 / 8Numbered / bulleted list.
Ctrl/Cmd+Z, Ctrl/Cmd+Shift+ZUndo, redo.
Arrow keys in the toolbarMove between toggles (follow the reading direction).

The content is role="textbox" with aria-multiline. Toolbar buttons have aria-labels (en/ar built in) and toggles expose aria-pressed. Point aria-labelledby at the visible label, or give aria-label. Tooltips carry the shortcut but are not the only name.

RTL & i18n

  • Each paragraph, heading, quote and list has dir="auto"; with text-start the block aligns to its own start.
  • The dir attribute is stored in the document, so HTML and JSON output keep it and Markdown-style renderers or emails orient correctly.
  • Toolbar labels, placeholder and link dialog are Arabic when the Nasaq locale is ar. The link address input is forced LTR.
  • Undo and redo icons mirror in RTL.

Styling & tokens

Content typography reuses the Markdown roles: text-h1..h3, text-body, text-nq-fg-body, border-nq-line-strong, bg-secondary for inline code. The frame uses border-input, bg-card and the focus outline token. Target data-slot values and data-readonly. Extend with className.

Do / Don't

  • Do lazy-load it.
  • Do sanitise the HTML on the server before storing or rendering it elsewhere; the editor only filters link schemes.
  • Don't put raw hex colours in pasted content styling.
  • Don't use it for short single-line input.

Lab

https://docs.nasaqui.com/?path=/docs/components-editors-richtexteditor--docs

On this page