openwrite
v0.1.0
Published
A modern, customizable WYSIWYG editor for React and Next.js
Maintainers
Readme
OpenWrite
A modern, lightweight WYSIWYG rich-text editor for React and Next.js, built on contentEditable with zero heavy dependencies.
Features
- Rich formatting — bold, italic, underline, strikethrough, sub/superscript, inline code
- Headings — H1–H6 with paragraph fallback
- Lists — ordered and unordered, with indent/outdent
- Alignment — left, center, right, justify
- Tables — insert and edit HTML tables
- Links & images — dialog-driven insertion
- Blockquotes & code blocks — with smart Enter-to-exit behavior
- Find & Replace — with case-sensitive mode and match navigation
- Import / Export — import
.docxfiles, export to.docxor.pdf - Floating toolbar — context-sensitive toolbar on text selection
- Word count — live word and character count footer
- Source view — toggle between WYSIWYG and raw HTML
- Fullscreen — distraction-free writing mode
- Themes —
lightanddark - Localization — built-in French and English; fully customizable
- Plugin API — add custom toolbar buttons with your own commands
- Next.js compatible — works with App Router (
'use client')
Installation
npm install openwriteBasic usage
import { OpenWriteEditor } from 'openwrite';
import 'openwrite/styles';
function MyPage() {
const [html, setHtml] = useState('');
return <OpenWriteEditor value={html} onChange={setHtml} />;
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Controlled HTML content |
| defaultValue | string | '' | Initial HTML (uncontrolled) |
| onChange | (html: string) => void | — | Called on every change |
| onFocus | () => void | — | Called on focus |
| onBlur | () => void | — | Called on blur |
| placeholder | string | 'Commencez à écrire…' | Placeholder text |
| theme | 'light' \| 'dark' | 'light' | Color theme |
| readOnly | boolean | false | Disable editing |
| toolbar | ToolbarConfig | DEFAULT_TOOLBAR | Toolbar layout |
| floatingToolbar | boolean | false | Show floating toolbar on selection |
| showWordCount | boolean | false | Show word/character count footer |
| minHeight | number | 300 | Minimum editor height (px) |
| maxHeight | number | — | Maximum editor height (px) |
| spellCheck | boolean | true | Browser spell check |
| autoFocus | boolean | false | Focus on mount |
| locale | 'fr' \| 'en' \| EditorLocale \| Partial<EditorLocale> | 'en' | UI locale |
| plugins | OpenWritePlugin[] | [] | Plugin list |
| className | string | — | Class on the outer wrapper |
| contentClassName | string | — | Class on the editable area |
Localization
OpenWrite ships with French and English built in. The locale prop accepts three forms:
// 1. Built-in preset (string)
<OpenWriteEditor locale="en" />
<OpenWriteEditor locale="en" /> // default
// 2. Partial override — merge on top of French defaults
<OpenWriteEditor locale={{ toolbar: { bold: 'Negrita' } }} />
// 3. Full custom locale
import type { EditorLocale } from 'openwrite';
const localeEs: EditorLocale = {
toolbar: { bold: 'Negrita (Ctrl+B)', /* ... */ },
editor: { placeholder: 'Empieza a escribir…', ariaLabel: 'Área de texto' },
findReplace: { searchPlaceholder: 'Buscar…', results: (c, t) => `${c}/${t}`, /* ... */ },
wordCount: {
words: (n) => `${n} ${n === 1 ? 'palabra' : 'palabras'}`,
characters: (n) => `${n} ${n === 1 ? 'carácter' : 'caracteres'}`,
},
linkDialog: { title: 'Insertar enlace', /* ... */ },
imageDialog: { title: 'Insertar imagen', /* ... */ },
};
<OpenWriteEditor locale={localeEs} />Import / Export
import { importFromWord, exportToWord, exportToPdf } from 'openwrite';
// Import a .docx file → HTML string
const html = await importFromWord(file);
// Export HTML → .docx download
await exportToWord(html, 'document.docx');
// Export a DOM element → .pdf download
await exportToPdf(document.getElementById('my-editor')!, 'document.pdf');The toolbar already includes import/export buttons by default (DEFAULT_TOOLBAR). Remove them by customizing the toolbar prop.
Toolbar customization
import { OpenWriteEditor, DEFAULT_TOOLBAR } from 'openwrite';
// Remove the import/export group
const toolbar = DEFAULT_TOOLBAR.filter(
(group) => !group.some((item) => ['importWord','exportWord','exportPdf'].includes(item as string))
);
<OpenWriteEditor toolbar={toolbar} />Imperative API (ref)
import { useRef } from 'react';
import type { EditorRef } from 'openwrite';
const ref = useRef<EditorRef>(null);
ref.current?.getHTML(); // returns current HTML
ref.current?.setHTML('<p>Hi</p>');
ref.current?.focus();
ref.current?.blur();
ref.current?.clear();
ref.current?.execCommand('bold'); // runs a document.execCommandPlugin API
import type { OpenWritePlugin } from 'openwrite';
const myPlugin: OpenWritePlugin = {
toolbarItems: [
{
key: 'my-button',
icon: <MyIcon />,
title: 'My action',
command: (api) => {
api.insertHTML('<span class="custom">✦</span>');
},
},
],
};
<OpenWriteEditor plugins={[myPlugin]} />Styling
Import the stylesheet once (usually in your root layout):
import 'openwrite/styles';The editor uses Tailwind CSS classes internally. Customize the appearance with className / contentClassName props or by overriding .ow-editor and .ow-editor-content in your own CSS.
Next.js App Router
Mark any component that renders OpenWriteEditor with 'use client':
'use client';
import { OpenWriteEditor } from 'openwrite';License
MIT
