react-zenith-editor
React Zenith Editor
React Zenith Editor is a lightweight, TypeScript-first rich text editor for React with two layers:
- a low-level editor component for full control
- a production-ready form field component for add and edit workflows
It is built to integrate cleanly with custom forms, React Hook Form, and Formik while keeping performance and developer experience strong.
Search Keywords
Use these searchable keywords for discoverability in docs portals, internal wikis, package search, and GitHub search:
react rich text editor, wysiwyg editor react, typescript rich text editor, form integrated editor, react hook form rich text, formik rich text editor, contenteditable editor, lightweight rich text editor, customizable toolbar editor, table resize drag rich text, image video upload editor, code block editor, react reusable editor component, html editor react, add edit mode form field
Packages
@react-zenith-editor/core- typed document model
- document utilities and serializers
react-zenith-editorEditorfor low-level rich editingRichTextFieldfor high-level form integration
Key Features
- Add mode and edit mode support
- Controlled and uncontrolled usage
- Rich toolbar: formatting, lists, links, media, tables, code blocks, print
- Drag and resize support for rich elements such as tables and media
- Hidden input support through
namefor native form submissions - Ref handles for imperative focus, read, write, clear, and validate actions
- Built-in required/custom validation via
RichTextField - Lightweight runtime dependency surface
Installation
This repository is a pnpm monorepo.
pnpm install
In project code:
import { Editor, RichTextField } from 'react-zenith-editor';
Quick Start
Minimal Editor
import { Editor } from 'react-zenith-editor';
export function BasicDemo() {
return <Editor placeholder="Start writing..." />;
}
Form-Ready Field
import { RichTextField } from 'react-zenith-editor';
import { useState } from 'react';
export function BasicFormField() {
const [body, setBody] = useState('');
return (
<RichTextField
name="body"
label="Body"
required
value={body}
onChange={setBody}
placeholder="Write your content..."
/>
);
}
Complete Use Cases
1) Add Mode
Use empty or default values for creating new content.
import { RichTextField, type RichTextFieldHandle } from 'react-zenith-editor';
import { useRef, useState } from 'react';
export function CreatePostForm() {
const fieldRef = useRef<RichTextFieldHandle>(null);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [error, setError] = useState<string | undefined>();
const onSubmit = (event: React.FormEvent) => {
event.preventDefault();
const validationError = fieldRef.current?.validate();
setError(validationError);
if (validationError) {
fieldRef.current?.focus();
return;
}
console.log({ title, content });
};
return (
<form onSubmit={onSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
/>
<RichTextField
ref={fieldRef}
name="content"
label="Content"
required
value={content}
onChange={setContent}
error={error}
placeholder="Write the post..."
/>
<button type="submit">Create</button>
</form>
);
}
2) Edit Mode
Pre-populate existing content and let users edit safely.
import { RichTextField } from 'react-zenith-editor';
import { useEffect, useState } from 'react';
type Post = {
id: string;
title: string;
bodyHtml: string;
};
export function EditPostForm({ post }: { post: Post }) {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
useEffect(() => {
setTitle(post.title);
setContent(post.bodyHtml);
}, [post]);
return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log({ id: post.id, title, content });
}}
>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<RichTextField
name="content"
label="Content"
value={content}
onChange={setContent}
/>
<button type="submit">Save</button>
</form>
);
}
3) Low-Level Editor Controlled by HTML
Use Editor directly if you want complete control.
import { Editor } from 'react-zenith-editor';
import { useState } from 'react';
export function ControlledEditor() {
const [html, setHtml] = useState('<p>Initial content</p>');
return (
<Editor
name="content"
html={html}
onHtmlChange={setHtml}
placeholder="Write..."
/>
);
}
Form Integration
React Hook Form
import { Controller, useForm } from 'react-hook-form';
import { RichTextField } from 'react-zenith-editor';
type FormValues = {
title: string;
body: string;
};
export function RhfExample() {
const { control, handleSubmit } = useForm<FormValues>({
defaultValues: {
title: '',
body: '<p>Default body</p>'
}
});
return (
<form onSubmit={handleSubmit(console.log)}>
<Controller
name="body"
control={control}
rules={{
validate: (value) => value.replace(/<[^>]*>/g, '').trim().length > 0 || 'Body is required'
}}
render={({ field, fieldState }) => (
<RichTextField
name={field.name}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
error={fieldState.error?.message}
label="Body"
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
}
Formik
import { Formik, Form } from 'formik';
import { RichTextField } from 'react-zenith-editor';
export function FormikExample() {
return (
<Formik
initialValues={{ body: '' }}
validate={(values) => {
const text = values.body.replace(/<[^>]*>/g, '').trim();
return text ? {} : { body: 'Body is required' };
}}
onSubmit={(values) => {
console.log(values);
}}
>
{({ values, errors, touched, setFieldValue, setFieldTouched }) => (
<Form>
<RichTextField
name="body"
label="Body"
value={values.body}
onChange={(html) => setFieldValue('body', html)}
onBlur={() => setFieldTouched('body', true)}
error={touched.body ? (errors.body as string | undefined) : undefined}
/>
<button type="submit">Submit</button>
</Form>
)}
</Formik>
);
}
Native Form Submission
Because Editor and RichTextField support name, they render a hidden input with the current HTML value.
<form method="post" action="/submit">
<RichTextField name="body" label="Body" defaultValue="<p>Hello</p>" />
<button type="submit">Submit</button>
</form>
API Reference
Editor
Props in Editor:
- Data and change
value?: DocumentModelonChange?: (value: DocumentModel) => voidhtml?: stringdefaultValue?: stringonHtmlChange?: (html: string) => void
- Form compatibility
name?: stringonBlur?: React.FocusEventHandler<HTMLDivElement>disabled?: booleanreadOnly?: boolean
- UX
placeholder?: string
- Toolbar config
toolbar?: ToolbarItem[]toolbarPosition?: 'top' | 'bottom' | 'left' | 'right' | 'floating' | 'inline'toolbarSticky?: booleantoolbarWrap?: booleantoolbarRounded?: booleantoolbarShadow?: booleantoolbarSize?: 'small' | 'medium' | 'large' | 'compact' | 'touch'toolbarVariant?: 'default' | 'filled' | 'outlined'toolbarTheme?: 'light' | 'dark' | 'minimal' | 'glass' | 'material' | 'bootstrap' | 'tailwind' | 'corporate'toolbarDisabledSections?: ToolbarSection[]toolbarFeatures?: ToolbarFeatureDefinition[]plugins?: EditorPlugin[]showTooltips?: booleanshowDividers?: booleaniconSize?: numberbuttonSize?: numbertoolbarGap?: number
- Media upload and validation
maxImageSizeMB?: numbermaxVideoSizeMB?: numberallowedImageTypes?: string[]allowedVideoTypes?: string[]onImageUpload?: (file: File) => Promise<string>onVideoUpload?: (file: File) => Promise<string>
Ref handle in EditorHandle:
getHTML(): stringsetHTML(html: string): voidgetText(): stringfocus(): voidclear(): voidisEmpty(): booleanelement: HTMLDivElement | null
RichTextField
Props in RichTextField:
- Value and events
value?: stringdefaultValue?: stringonChange?: (html: string) => voidonBlur?: React.FocusEventHandler<HTMLDivElement>
- Form behavior
name?: stringrequired?: booleanrequiredMessage?: stringvalidate?: (html: string) => string | undefinederror?: string
- Presentation
label?: React.ReactNodeplaceholder?: stringhelperText?: React.ReactNodedisabled?: booleanreadOnly?: booleanid?: stringclassName?: stringstyle?: React.CSSPropertieslabelStyle?: React.CSSPropertieserrorStyle?: React.CSSProperties
- Pass-through editor config
editorProps?: Omit<EditorProps, 'value' | 'onChange' | 'html' | 'defaultValue' | 'onHtmlChange' | 'onBlur' | 'name' | 'disabled' | 'readOnly' | 'placeholder'>
Ref handle in RichTextFieldHandle:
focus(): voidclear(): voidgetHTML(): stringsetHTML(html: string): voidisEmpty(): booleanvalidate(): string | undefined
Toolbar Customization
Restrict toolbar items
import { Editor } from 'react-zenith-editor';
const toolbar = ['bold', 'italic', 'underline', 'link', 'orderedList', 'bulletList'] as const;
export function LimitedToolbar() {
return <Editor toolbar={[...toolbar]} />;
}
Add custom toolbar button
import { Editor, type ToolbarItem } from 'react-zenith-editor';
const customToolbar: ToolbarItem[] = [
'bold',
'italic',
{
id: 'insertDate',
tooltip: 'Insert date',
icon: <span style={{ fontSize: 12 }}>DATE</span>,
onClick: ({ runCommand }) => {
runCommand('insertHTML', `<span>${new Date().toISOString().slice(0, 10)}</span>`);
}
}
];
export function CustomToolbar() {
return <Editor toolbar={customToolbar} />;
}
Upload Integration
import { Editor } from 'react-zenith-editor';
async function uploadAsset(file: File): Promise<string> {
const body = new FormData();
body.append('file', file);
const response = await fetch('/api/upload', { method: 'POST', body });
const data = await response.json();
return data.url;
}
export function UploadReadyEditor() {
return (
<Editor
onImageUpload={uploadAsset}
onVideoUpload={uploadAsset}
maxImageSizeMB={10}
maxVideoSizeMB={100}
/>
);
}
Core Document Model
import { createDocument, insertParagraph, toggleMark, toHTML, toMarkdown } from '@react-zenith-editor/core';
const doc = createDocument('Hello');
const doc2 = insertParagraph(doc, doc.blocks[0].id);
const doc3 = toggleMark(doc2, doc2.blocks[0].id, 'bold');
console.log(toHTML(doc3));
console.log(toMarkdown(doc3));
Performance Guidance
- Prefer controlled mode only when the parent truly needs immediate HTML state on every change.
- Use uncontrolled mode (
defaultValue) for simple forms to reduce parent re-renders. - Memoize heavy parent components around the editor.
- Avoid duplicating the same editor value in multiple local states.
- Use
RichTextFieldfor built-in validation and lower integration boilerplate.
Accessibility Notes
- The editor surface exposes
role="textbox"andaria-multiline. RichTextFieldprovides label and error semantics suitable for form usage.- Keep labels meaningful and pass explicit
idwhen integrating into complex layouts.
Troubleshooting
- Font family or size not applying
- Ensure the text is selected before choosing a style.
- In custom integrations, avoid manually stealing focus between selection and style actions.
- Empty content validation mismatch
- Validate text after stripping tags if your backend requires plain text.
- Native form submit does not include editor value
- Ensure
nameis provided onEditororRichTextField.
- Ensure
Development
pnpm install
pnpm build
pnpm test
Build and Publish
Build all publishable packages:
pnpm install
pnpm build
Verify package contents before publish:
cd packages/react
npm pack --dry-run
Publish to npm (public package):
cd packages/react
npm publish --access public
Notes:
packages/react/package.jsonis the package that publishes asreact-zenith-editor.packages/core/package.jsonis markedprivate: true, so it will not be published.- If
0.1.0is already published, bump the version before runningnpm publish.