npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-zenith-editor

v0.1.1

Published

React Zenith Editor is a lightweight, TypeScript-first rich text editor for React with two layers:

Readme

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-editor
    • Editor for low-level rich editing
    • RichTextField for 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 name for 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?: DocumentModel
    • onChange?: (value: DocumentModel) => void
    • html?: string
    • defaultValue?: string
    • onHtmlChange?: (html: string) => void
  • Form compatibility
    • name?: string
    • onBlur?: React.FocusEventHandler<HTMLDivElement>
    • disabled?: boolean
    • readOnly?: boolean
  • UX
    • placeholder?: string
  • Toolbar config
    • toolbar?: ToolbarItem[]
    • toolbarPosition?: 'top' | 'bottom' | 'left' | 'right' | 'floating' | 'inline'
    • toolbarSticky?: boolean
    • toolbarWrap?: boolean
    • toolbarRounded?: boolean
    • toolbarShadow?: boolean
    • toolbarSize?: '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?: boolean
    • showDividers?: boolean
    • iconSize?: number
    • buttonSize?: number
    • toolbarGap?: number
  • Media upload and validation
    • maxImageSizeMB?: number
    • maxVideoSizeMB?: number
    • allowedImageTypes?: string[]
    • allowedVideoTypes?: string[]
    • onImageUpload?: (file: File) => Promise<string>
    • onVideoUpload?: (file: File) => Promise<string>

Ref handle in EditorHandle:

  • getHTML(): string
  • setHTML(html: string): void
  • getText(): string
  • focus(): void
  • clear(): void
  • isEmpty(): boolean
  • element: HTMLDivElement | null

RichTextField

Props in RichTextField:

  • Value and events
    • value?: string
    • defaultValue?: string
    • onChange?: (html: string) => void
    • onBlur?: React.FocusEventHandler<HTMLDivElement>
  • Form behavior
    • name?: string
    • required?: boolean
    • requiredMessage?: string
    • validate?: (html: string) => string | undefined
    • error?: string
  • Presentation
    • label?: React.ReactNode
    • placeholder?: string
    • helperText?: React.ReactNode
    • disabled?: boolean
    • readOnly?: boolean
    • id?: string
    • className?: string
    • style?: React.CSSProperties
    • labelStyle?: React.CSSProperties
    • errorStyle?: React.CSSProperties
  • Pass-through editor config
    • editorProps?: Omit<EditorProps, 'value' | 'onChange' | 'html' | 'defaultValue' | 'onHtmlChange' | 'onBlur' | 'name' | 'disabled' | 'readOnly' | 'placeholder'>

Ref handle in RichTextFieldHandle:

  • focus(): void
  • clear(): void
  • getHTML(): string
  • setHTML(html: string): void
  • isEmpty(): boolean
  • validate(): 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 RichTextField for built-in validation and lower integration boilerplate.

Accessibility Notes

  • The editor surface exposes role="textbox" and aria-multiline.
  • RichTextField provides label and error semantics suitable for form usage.
  • Keep labels meaningful and pass explicit id when 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 name is provided on Editor or RichTextField.

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.json is the package that publishes as react-zenith-editor.
  • packages/core/package.json is marked private: true, so it will not be published.
  • If 0.1.0 is already published, bump the version before running npm publish.