mandoo-editor
v1.2.0
Published
A modern, lightweight WYSIWYG rich text editor for React & Next.js — feature-flagged, fully typed, zero runtime dependencies.
Maintainers
Readme
MandooEditor
Features
| Feature | Description |
| -------------------- | --------------------------------------------------------------------------------- |
| Visual editor | contenteditable WYSIWYG — no iframe, no Flash |
| Block mode | Drag-and-drop block editor with per-block type selector |
| Text / HTML mode | Raw HTML editing with syntax highlighting |
| HTML & Markdown | onChange fires in whichever format you choose |
| Feature flags | Enable or disable every toolbar button individually |
| Media upload | Wire any S3 / MinIO / custom API — just pass callbacks |
| Plugins | Link checker, tables, image editor, history, YouTube embed, subscript/superscript |
| Fully typed | End-to-end TypeScript with imperative ref handle |
| Zero deps | No runtime dependencies beyond React |
Installation
npm install mandoo-editor
# or
yarn add mandoo-editor
# or
pnpm add mandoo-editor⚠️ Required — add this import wherever you use the editor:
import 'mandoo-editor/styles';Add it in your layout, page, or component — wherever MandooEditor is rendered. Without it the editor has no styling.
Quick Start
"use client";
import MandooEditor from "mandoo-editor";
export default function MyPage() {
return (
<MandooEditor
defaultValue="<p>Start writing...</p>"
onChange={(html) => console.log(html)}
height={400}
/>
);
}API Reference
Props
| Prop | Type | Default | Description |
| -------------- | ------------------------- | --------------------------- | ---------------------------------------- |
| value | string | — | Controlled HTML value |
| defaultValue | string | '' | Uncontrolled initial HTML value |
| onChange | (value: string) => void | — | Fires on every change with current value |
| outputFormat | 'html' \| 'markdown' | 'html' | Format for onChange and getValue() |
| placeholder | string | 'Start writing…' | Placeholder shown when empty |
| tabs | TabId[] | ['visual','text','block'] | Which tabs to display |
| defaultTab | TabId | 'visual' | Initially active tab |
| features | Features | all enabled | Granular toolbar feature flags |
| plugins | Plugins | none | Optional plugin flags |
| media | MediaConfig | — | File upload / library config |
| theme | 'classic' \| 'modern' | 'classic' | Visual theme |
| colorScheme | 'light' \| 'dark' | 'light' | Color scheme |
| defaultDir | 'rtl' \| 'ltr' | — | Default text direction for the editor |
| height | number | 400 | Min height of editor content area (px) |
| className | string | — | Extra CSS class on root element |
| apiToken | string | — | Token for future paid pro features |
Imperative Handle (ref)
import { useRef } from "react";
import MandooEditor, { MandooEditorHandle } from "mandoo-editor";
const ref = useRef<MandooEditorHandle>(null);
// Methods:
ref.current?.getValue(); // → string (respects outputFormat)
ref.current?.getHTML(); // → raw HTML string
ref.current?.getMarkdown(); // → Markdown string
ref.current?.setValue(html); // set content programmatically
ref.current?.focus(); // focus the editor
ref.current?.clear(); // clear contentForm Integration
MandooEditor outputs HTML or Markdown. There are two ways to use it in a form:
Option 1 — name prop (native forms, FormData, Server Actions)
Add a name prop and a hidden <input> is automatically rendered. Works with any form library or native HTML form submission.
// Native HTML form
<form action="/api/save" method="POST">
<MandooEditor name="content" outputFormat="html" />
<button type="submit">Save</button>
</form>
// Next.js Server Action
async function save(formData: FormData) {
'use server';
const content = formData.get('content'); // ← HTML or Markdown
}
<form action={save}>
<MandooEditor name="content" outputFormat="markdown" />
<button type="submit">Save</button>
</form>Option 2 — onChange (controlled state, react-hook-form, Zustand…)
// useState
const [content, setContent] = useState('');
<MandooEditor onChange={setContent} outputFormat="html" />
// react-hook-form
const { setValue } = useForm();
<MandooEditor onChange={(v) => setValue('content', v)} outputFormat="markdown" />
// Zustand / Redux
<MandooEditor onChange={(v) => dispatch(setContent(v))} />Feature Flags
Disable any toolbar button by setting its flag to false:
<MandooEditor
features={{
// Disable specific buttons
strikethrough: false,
align: false,
charMap: false,
help: false,
// All others remain enabled
}}
/>Full list of flags: bold, italic, strikethrough, lists, blockquote, hr, align, link, code, direction, fullscreen, kitchenSink, underline, justify, foreColor, pasteAsText, removeFormat, charMap, indent, undo, help, media, subscript, superscript
Code Formatting
The code feature adds a Code button to the toolbar. It has two modes depending on the selection:
| Context | Result |
|---|---|
| Text selected | Wraps in inline <code> |
| No selection / cursor in a block | Converts block to <pre> (code block) |
| Click again inside <code> or <pre> | Removes the formatting |
Both <code> and <pre> share the same visual style — monospace font, subtle background from --me-textarea-bg, and a matching border — so inline and block code look like a family.
// Disable the code button
<MandooEditor features={{ code: false }} />RTL / LTR Direction
The direction feature adds RTL and LTR toggle buttons to the toolbar. Direction is applied per block — each paragraph or heading can have its own direction independently.
| Action | Result |
|---|---|
| Click RTL | Sets dir="rtl" style="direction:rtl; text-align:right" on the current block |
| Click LTR | Sets dir="ltr" style="direction:ltr; text-align:left" on the current block |
| Click the active button again | Removes direction from the block (toggle off) |
One button is always highlighted: the active block's direction, or defaultDir if set, or LTR by default.
// RTL-first editor (e.g. Persian / Arabic content)
<MandooEditor defaultDir="rtl" />
// Disable the direction buttons entirely
<MandooEditor features={{ direction: false }} />Direction is stored inline in the HTML output so it renders correctly anywhere, without requiring the editor's stylesheet:
<p dir="rtl" style="direction: rtl; text-align: right;">متن فارسی</p>
<p dir="ltr" style="direction: ltr; text-align: left;">English paragraph</p>Plugins
<MandooEditor
plugins={{
linkChecker: true, // Validate URLs when inserting links
spellChecker: true, // Browser-native spell check
tables: true, // Insert & edit tables
imageEditor: true, // Crop/resize images before upload
history: true, // Edit history with restore
youtube: true, // Embed YouTube videos by URL
}}
/>Media Upload
Wire any storage backend — S3, MinIO, Cloudflare R2, or your own API:
<MandooEditor
media={{
accept: "image/*,video/*",
maxSize: 10 * 1024 * 1024, // 10 MB
async onUpload(file) {
const fd = new FormData();
fd.append("file", file);
const res = await fetch("/api/upload", { method: "POST", body: fd });
return res.json(); // { url: string, name?: string, alt?: string }
},
async onListFiles() {
const res = await fetch("/api/media");
return res.json(); // MediaFile[]
},
}}
/>MinIO / S3 Server Route (Next.js App Router)
// app/api/upload/route.ts
import { Client } from "minio"; // npm install minio
import { NextRequest, NextResponse } from "next/server";
const minio = new Client({
endPoint: process.env.MINIO_ENDPOINT!,
useSSL: true,
accessKey: process.env.MINIO_ACCESS_KEY!,
secretKey: process.env.MINIO_SECRET_KEY!,
});
export async function POST(req: NextRequest) {
const form = await req.formData();
const file = form.get("file") as File;
const buf = Buffer.from(await file.arrayBuffer());
const name = `uploads/${Date.now()}-${file.name}`;
await minio.putObject(process.env.MINIO_BUCKET!, name, buf, buf.length, {
"Content-Type": file.type,
});
const url = await minio.presignedGetObject(
process.env.MINIO_BUCKET!,
name,
604800
);
return NextResponse.json({ url, name: file.name });
}Output Formats
// HTML output (default)
<MandooEditor
outputFormat="html"
onChange={(html) => {
// "<p>Hello <strong>world</strong></p>"
console.log(html);
}}
/>
// Markdown output
<MandooEditor
outputFormat="markdown"
onChange={(md) => {
// "Hello **world**"
console.log(md);
}}
/>Tabs Configuration
// Only show Visual and Text tabs (no Block editor)
<MandooEditor tabs={['visual', 'text']} />
// Start on Block tab
<MandooEditor defaultTab="block" />
// Only Block editor
<MandooEditor tabs={['block']} />Theming
Built-in themes
MandooEditor ships with two visual themes and two color schemes — mix and match any combination:
// Classic theme (default) — dense toolbar, serif content font
<MandooEditor theme="classic" colorScheme="light" />
// Classic dark
<MandooEditor theme="classic" colorScheme="dark" />
// Modern theme — minimal toolbar, rounded corners, sans-serif content font
<MandooEditor theme="modern" colorScheme="light" />
// Modern dark
<MandooEditor theme="modern" colorScheme="dark" />CSS customization
Every color, radius, and font in MandooEditor is driven by CSS custom properties set on the root container. You can override any of them from your own CSS:
/* globals.css or any stylesheet loaded after mandoo-editor/styles */
.mandoo-editor-container {
--me-accent: #e11d48; /* links, active buttons, focus rings */
--me-container-radius: 4px; /* outer border radius */
--me-content-font: 'Vazirmatn', sans-serif; /* content area font */
}You can also scope overrides to a specific theme or color scheme:
/* Only affect the modern theme */
.mandoo-editor-container[data-mandoo-theme="modern"] {
--me-accent: #7c3aed;
--me-toolbar-bg: #fafafa;
}
/* Only affect dark mode */
.mandoo-editor-container[data-mandoo-scheme="dark"] {
--me-bg: #18181b;
--me-border: #27272a;
}Full list of CSS variables
| Variable | Controls | Classic light default |
|---|---|---|
| --me-bg | Editor & modal background | #ffffff |
| --me-border | All borders | #dddddd |
| --me-color | UI text | #444444 |
| --me-color-strong | Headings, modal titles | #23282d |
| --me-toolbar-bg | Toolbar row background | #ebebeb |
| --me-tools-bg | Media/tabs bar background | #f1f1f1 |
| --me-btn-hover | Button hover background | #d5d5d5 |
| --me-btn-active | Active/pressed button background | #b8b8b8 |
| --me-btn-bg | Inactive button background | #f3f5f6 |
| --me-statusbar-bg | Status bar background | #ebebeb |
| --me-textarea-bg | HTML textarea background | #f9f9f9 |
| --me-modal-bg | Modal body background | #ffffff |
| --me-modal-header-bg | Modal header background | #f1f1f1 |
| --me-accent | Links, focus rings, active state | #0073aa |
| --me-muted | Placeholder, counts, labels | #888888 |
| --me-sep | Toolbar separators | #cccccc |
| --me-content-color | Content area text | #333333 |
| --me-content-font | Content area font family | Georgia, serif |
| --me-btn-size | Toolbar button width & height | 26px |
| --me-btn-radius | Toolbar button border radius | 2px |
| --me-container-radius | Outer container border radius | 0px |
Pro Features (Coming Soon)
The following features require an apiToken and will be available in a future paid tier:
- Export to PDF — one-click export via Mandoo cloud API
- Word Import/Export — read and write
.docxfiles - AI Assistant — chat with AI to rewrite, summarise, or extend content
// Reserve your token now — setting it has no effect until pro plugins are released
<MandooEditor apiToken="mk_live_..." />Token Infrastructure
import { mandooFetch, validateToken } from "mandoo-editor";
// Validate a token format
const valid = validateToken("mk_live_abc123...");
// Call Mandoo API (for pro features)
const result = await mandooFetch(
"/export/pdf",
{ method: "POST", body: fd },
{
token: "mk_live_...",
baseUrl: "https://api.mandooeditor.com/v1", // optional override
}
);TypeScript Types
import type {
MandooEditorProps,
MandooEditorHandle,
Features,
Plugins,
MediaConfig,
MediaFile,
MediaUploadResult,
TabId,
OutputFormat,
TokenConfig,
} from "mandoo-editor";Links
| | | | -------------- | ------------------------------------------------------------------------------------------------ | | 🌍 Website | mandooeditor.markrahimi.com | | 📦 npm | npmjs.com/package/mandoo-editor | | 🐙 GitHub | github.com/markrahimi/mandoo-editor | | 🐛 Issues | github.com/markrahimi/mandoo-editor/issues | | ☕ Support | ko-fi.com/markrahimi | | 👤 Author | markrahimi.com |
License
MIT © Mohammad Ali Rahimi
