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

smartrte-react

v1.1.1

Published

A powerful, feature-rich Rich Text Editor for React with support for tables, mathematical formulas (LaTeX/KaTeX), and media management

Readme

smartrte-react

npm version License: MIT

A rich text editor for React, built on a document-model core rather than raw contentEditable state — tables, LaTeX/KaTeX formulas, media, DOCX/PDF/Markdown import-export, per-tool toolbar visibility, and host-owned integration points for version history, comments, and suggestions/track-changes.

It pairs with smartrte-core, a framework-agnostic document engine — you only need this package to use it from React.

Contents

Install

npm install smartrte-react
# or: pnpm add smartrte-react / yarn add smartrte-react

react and react-dom (>=18) are peer dependencies — install them if your project doesn't already have them. No separate CSS import is required; the editor injects its own stylesheet on mount.

Quick start

import { useState } from "react";
import { CanonicalAuthorityEditor } from "smartrte-react";

function App() {
  const [content, setContent] = useState("<p>Start typing…</p>");

  return (
    <CanonicalAuthorityEditor
      defaultValue={content}
      onHtmlChange={setContent}
      placeholder="Type here…"
    />
  );
}

defaultValue is uncontrolled — it seeds the editor once on mount, not on every render (see Props for why, and how to programmatically replace content later via the imperative handle).

Which component do I use? CanonicalAuthorityEditor vs ClassicEditor

  • CanonicalAuthorityEditor — the actual editor. Everything in this guide (tools, providers, presets, the imperative handle) is its API. Use this for anything new.
  • ClassicEditor — a thin backwards-compatibility wrapper around CanonicalAuthorityEditor, kept for integrations written against the package's older value/onChange: (html: string) => void shape. It forwards everything it can (tools, mediaProvider, versionProvider, etc.) but silently ignores a handful of props from an even older, now-retired plugin system (features, plugins, formats, fonts, theme, mediaManager as an adapter object). If you're starting fresh, use CanonicalAuthorityEditor directly — ClassicEditor exists so old call sites keep compiling, not as a recommended entry point.
// Legacy-compatible shape - only use this if migrating an existing integration
import { ClassicEditor } from "smartrte-react";

<ClassicEditor value={htmlString} onChange={(html) => setHtmlString(html)} />

Props

The commonly-used CanonicalAuthorityEditor props:

| Prop | Type | Default | Description | |---|---|---|---| | defaultValue | string \| PersistedEditorDocument | undefined | Initial content (HTML string or a previously-saved document envelope). Uncontrolled after mount — see Imperative handle to replace content later. | | onChange | (change: SmartEditorChange) => void | undefined | Fires per transaction with the structured change event. | | onHtmlChange | (html: string) => void | undefined | Debounced (~250ms after the last edit) plain-HTML serialization — the simplest way to persist content as a string. | | preset | "full" \| "simple" | "full" | Construction-time capability preset — "simple" excludes the table plugin entirely (schema-level, not just hidden in the toolbar). See Capability presets. | | tools | Partial<ToolbarTools> | every tool true | Hide individual toolbar tools without touching document capability. See Toolbar customization. | | mediaProvider | MediaProvider | undefined | Host-owned upload/search/remove boundary for images, video, and audio. Absent ⇒ media tools don't render. | | mediaManager | boolean | true when mediaProvider is set | Use the library/search/duplicate-detection picker for images (vs. the plain file-input default). | | mediaPicker | MediaPickerComponent | built-in file picker | Replace the default file-picker UI for video/audio (and images, if mediaManager is false). | | versionProvider | VersionProvider | undefined | Host-owned save/list/load/remove boundary for version history. Absent ⇒ Version History tool doesn't render. | | commentProvider | CommentProvider | undefined | Host-owned boundary for comment threads. Absent ⇒ comment tools/markers don't render. | | suggestionProvider | SuggestionProvider | undefined | Host-owned boundary for structural suggestions (track-changes). Absent ⇒ suggestion tools/markers don't render. | | authorId | string | "anonymous" | Attributed to new comment replies and suggestions. | | renderFormulaHtml | boolean | false | Bake real KaTeX-rendered HTML into onHtmlChange's formula markup instead of an empty placeholder — turn this on if you render that HTML anywhere outside the editor (email, PDF export, a read-only view without KaTeX loaded). | | onClipboardDiagnostic | (report: ClipboardDiagnosticReport) => void | undefined | Inspect what a paste was parsed as / why it was rejected — useful while debugging a host's own copy sources. | | placeholder | string | undefined | Placeholder text shown when the editor is empty. | | minHeight / maxHeight | number \| string | undefined | Editing-surface height bounds. | | readOnly | boolean | false | Disables editing; toolbar tools become inert. | | className | string | undefined | Extra class(es) on the editor's root element — this is also how you enable dark mode. | | onRuntime | (runtime: CanonicalEditorRuntime) => void | undefined | Escape hatch for tests/diagnostics; not part of the stable editing contract. |

ClassicEditor accepts the same props under value/onChange: (html) => void instead of defaultValue/onHtmlChange, plus a legacy table?: boolean (equivalent to preset={table === false ? "simple" : "full"}).

Toolbar customization

tools hides individual toolbar entries — Bold, Video, Version history, whatever you name — without touching what the document itself can store. Every tool defaults to visible; only name the ones you want off:

<CanonicalAuthorityEditor
  tools={{ video: false, audio: false, versionHistory: false, comments: false, suggestions: false }}
/>

tools can only ever hide something, never conjure it into existence — a tool still needs its underlying capability to actually be there:

  • image/video/audio also need mediaProvider configured.
  • versionHistory also needs versionProvider; comments needs commentProvider; suggestions needs suggestionProvider.
  • insertTable also needs the table capability enabled (i.e. you haven't set preset="simple").

This means it's always safe to leave tools unset — a consumer who never passes it sees every tool their other configuration already supports.

Every toggleable key, grouped the way they appear in the toolbar:

interface ToolbarTools {
  // Text formatting
  bold: boolean; italic: boolean; underline: boolean; strikethrough: boolean; code: boolean;
  superscript: boolean; subscript: boolean; textColor: boolean; backgroundColor: boolean;
  fontSize: boolean; fontFamily: boolean;

  // Paragraph
  blockType: boolean;   // the Paragraph/Heading 1-6/Code block dropdown
  alignLeft: boolean; alignCenter: boolean; alignRight: boolean; alignJustify: boolean;
  lineHeight: boolean;  // the line-spacing dropdown (1/1.15/1.5/2/2.5 presets plus a custom value)
  quote: boolean;

  // Lists
  bulletedList: boolean; numberedList: boolean; checklist: boolean;
  listPreset: boolean;   // the named marker-preset picker (decimal/alpha/roman/outline/bullet glyphs)

  // Insert
  link: boolean; removeLink: boolean;
  image: boolean; video: boolean; audio: boolean;   // each requires mediaProvider
  insertFormula: boolean; specialCharacters: boolean;
  horizontalLine: boolean;   // inserts a divider (<hr>)
  pageBreak: boolean;        // a print/export pagination marker, distinct from horizontalLine
  insertTable: boolean;      // requires the table capability (preset)

  // Document
  import: boolean;
  saveAsHtml: boolean; saveAsMarkdown: boolean; saveAsWord: boolean; saveAsPdf: boolean; saveAsSmartRte: boolean;
  versionHistory: boolean;   // requires versionProvider
  comments: boolean;         // requires commentProvider
  suggestions: boolean;      // requires suggestionProvider

  // History
  undo: boolean; redo: boolean;
}

Purely contextual actions that only ever act on something already selected — moving a block up/down, indenting a list item, adding/removing a table row, resizing a selected image — aren't individually toggleable; they follow their owning tool's visibility (turn off insertTable and its row/column actions go with it, with no separate flag to remember).

Capability presets (table on/off)

preset is a construction-time, host/integrator-level setting (there's no in-editor UI for a user to change their own preset) — it decides which plugins the document's schema is built with, not just what the toolbar shows:

<CanonicalAuthorityEditor preset="simple" />   // excludes the table plugin entirely
<CanonicalAuthorityEditor preset="full" />     // default - excludes nothing

"simple" exists for content that should never contain tables at all (e.g. a short-answer question editor) — preset="simple" and tools={{ insertTable: false }} are not equivalent: the latter only hides the button, the former means the schema itself will reject a pasted or imported table.

Host-owned providers (media, versions, comments, suggestions)

Four features are opt-in via a provider interface the host implements — the package never holds storage credentials, a socket, or a database connection itself. Absent provider ⇒ that feature's toolbar entries simply don't render; nothing crashes or shows a broken control.

interface MediaProvider {
  upload(file: File, opts?: { signal?: AbortSignal }): Promise<{ url: string; id: string }>;
  search(query: string, filters?: MediaFilters, page?: number): Promise<MediaItem[]>;
  remove(id: string): Promise<void>;
}

interface VersionProvider {
  save(version: DocumentVersion): Promise<VersionListEntry>;
  list(): Promise<readonly VersionListEntry[]>;
  load(id: string): Promise<DocumentVersion>;
  remove(id: string): Promise<void>;
}

interface CommentProvider {
  list(): Promise<readonly CommentThread[]>;
  save(thread: CommentThread): Promise<void>;
  remove(threadId: string): Promise<void>;
}

interface SuggestionProvider {
  list(): Promise<readonly StructuralSuggestion[]>;
  save(suggestion: StructuralSuggestion): Promise<void>;
  remove(suggestionId: string): Promise<void>;
}
<CanonicalAuthorityEditor
  mediaProvider={{
    async upload(file) {
      const body = new FormData();
      body.append("file", file);
      const res = await fetch("/api/media", { method: "POST", body });
      return res.json(); // { url, id }
    },
    async search(query) {
      const res = await fetch(`/api/media?q=${encodeURIComponent(query)}`);
      return res.json();
    },
    async remove(id) {
      await fetch(`/api/media/${id}`, { method: "DELETE" });
    },
  }}
/>

Your upload implementation must independently validate file type, size, and content server-side — the editor applies only a best-effort client-side allow-list check as a UX nicety, not a security boundary.

There's a fifth contract, CollabTransport (real-time multi-writer editing), exported for hosts building against it — it defines sendTransaction/onRemoteTransaction/onPresenceUpdate/sendPresence/getRevisionHistory, but isn't wired into the editor's runtime yet. Without one connected, the editor behaves exactly as it does today: single-writer, no rebase path ever triggers.

Imperative handle (ref)

CanonicalAuthorityEditor/ClassicEditor forward a SmartEditorHandle ref for everything defaultValue/props alone can't do — replacing content programmatically, reading the current document, and version snapshots:

import { useRef } from "react";
import { CanonicalAuthorityEditor, type SmartEditorHandle } from "smartrte-react";

function Editor() {
  const ref = useRef<SmartEditorHandle>(null);

  const loadDocument = (doc) => ref.current?.replaceValue(doc, { keepSelection: false });
  const currentDoc = () => ref.current?.getValue();

  return <CanonicalAuthorityEditor ref={ref} />;
}
interface SmartEditorHandle {
  getValue(): PersistedEditorDocument;
  replaceValue(doc: PersistedEditorDocument, opts?: { keepSelection?: boolean }): void;
  isDirty(): boolean;
  markSaved(revision: number): void;
  getRevision(): number;
  focus(): void;
  executeOperations(operations: readonly SmartOperation[], opts?: ExecuteOperationsOptions): void;
  createCheckpoint(): SmartEditorCheckpoint;
  restoreCheckpoint(checkpoint: SmartEditorCheckpoint): void;
  saveVersion(opts?: { label?: string; authorId?: string }): DocumentVersion;
  restoreVersion(version: DocumentVersion, opts?: { keepSelection?: boolean }): void;
}

saveVersion/restoreVersion are the same operations the toolbar's Version History panel calls — use them directly if you want your own save-version UI instead of (or alongside) the built-in one.

Import & export formats

The toolbar's "Import" and "Save as ..." tools cover HTML, Markdown, DOCX (Word), PDF, and the package's own JSON document format out of the box — no extra setup. DOCX import preserves real Word styling (fonts, colors, spacing) where possible; PDF export prints the same HTML the editor renders, so formulas, tables, and images all appear as they do live.

For a custom import/export pipeline (e.g. converting on a server, or a "Save as..." flow outside the toolbar), the underlying codecs are re-exported from smartrte-core/foundation: exportDocxDocument, importDocxDocumentWithMammoth, importStyledDocxDocument, buildPdfPrintDocument, importPdfDocument, and the format-fidelity contract (builtInFormatFidelity) describing exactly what's lossless vs. lossy per format.

Theming

The editor uses CSS custom properties for every color — there's no theme prop; dark mode is a CSS class.

<CanonicalAuthorityEditor className="srte-dark" />
/* Or follow system preference yourself and toggle the class conditionally */
@media (prefers-color-scheme: dark) {
  .srte-editor:not(.srte-dark) { /* your own light/dark logic here */ }
}

Override individual variables (scoped to your own class, composed alongside srte-dark or standalone) to build a custom palette:

.my-theme {
  --srte-background: #1a1a2e;
  --srte-foreground: #eaeaea;
  --srte-border: #3a3a5c;
  --srte-accent: #7c3aed;
  /* override only what you need - everything else falls back to the default */
}

| Variable | Description | |---|---| | --srte-background / --srte-canvas | Toolbar/chrome background vs. editing-surface background | | --srte-foreground / --srte-muted-foreground | Primary vs. secondary text | | --srte-border | Standard border color | | --srte-ring | Focus ring color | | --srte-accent / --srte-accent-bg | Selection/active-state color and its translucent background | | --srte-primary / --srte-on-primary | Primary action button background/text | | --srte-danger | Destructive action color | | --srte-modal-bg / --srte-modal-backdrop | Dialog background and overlay | | --srte-menu-bg / --srte-menu-shadow | Dropdown/context-menu background and shadow | | --srte-code-bg / --srte-code-text | Code block colors |

These fall back to sensible defaults, and also read from common shadcn/ui-style tokens (--card, --background, --foreground, --muted, --border, --ring) if your app already defines those — so a Tailwind/shadcn app may need no overrides at all. Colors set via the color picker (text/background) are inline styles on content and are unaffected by theming — only editor chrome (toolbar, dialogs, menus) is themed.

Standalone / non-React embed

For a host that isn't a React app (or embeds via WebView — the Flutter package uses exactly this), a global-script build is available:

import "smartrte-react/standalone/classic-editor-embed";

window.SmartRTE.ClassicEditor.init({
  target: document.getElementById("editor"),
  value: "<p>Hello</p>",
  tools: { video: false, audio: false },
  onChange: (html) => console.log(html),
});

Returns a controller: { setHtml, getHtml, focus, blur, destroy }.

Security

The editor outputs HTML and never persists anything itself — storage, credentials, and the actual save are always the host's. Pasted HTML is sanitized on the way in (DOMPurify), but always sanitize before rendering elsewhere: if you take the editor's HTML output and dangerouslySetInnerHTML it in a different context (an email, a public page), treat it the same as any other user-generated HTML.

import DOMPurify from "dompurify";

function DisplayContent({ html }: { html: string }) {
  return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}

Found a security issue? Please email [email protected] rather than opening a public issue.

Browser support

Chromium, Firefox, and WebKit (Safari) — the full end-to-end suite runs against all three, headless and current, on every change.

Development

This package lives in a pnpm workspace monorepo alongside smartrte-core.

git clone https://github.com/ayush1852017/smart-rte.git
cd smart-rte
pnpm install
pnpm build          # builds every package
# Live playground (aliased to workspace source, not the built dist - edits hot-reload)
cd packages/react/playground
pnpm install
pnpm dev             # http://localhost:5173
# From packages/react
pnpm test            # vitest unit suite
pnpm e2e             # Playwright, all 3 browsers
pnpm storybook       # component stories, http://localhost:6006

Contributing

Issues and PRs are welcome at github.com/ayush1852017/smart-rte. For a bug report, include a minimal repro, expected vs. actual behavior, and your browser/OS. For a PR: keep it focused on one change, add test coverage (unit and/or a Playwright spec, matching whichever existing test file is closest to what you touched), and run pnpm build && pnpm test before pushing.

License

MIT — see LICENSE.