Getting Started
StesuraEditor renders the whole editor: toolbar, document canvas, floating
menus and side panels. You own the EditorState and pass it in with a
dispatchTransaction; a mount hook produces both. Every mount hook returns the
same { editorState, dispatch, pluginFactory, schema }, so you can start
in memory and swap the hook later without touching the editor, the plugins or
the UI extensions.
This page gets a minimal editor on screen, then grows it:
- Minimal editor —
useLocalEditor, in-memory, no backend. - Add features — plugins + UI extensions, à la carte.
- Add persistence or collaboration — swap the mount hook.
Install
npm install @stesura/editor-react @stesura/coreThe package scope is @stesura/* inside this monorepo. Versions of all
@stesura/* packages move together. In a Next.js app, list every
@stesura/* package you import (plus their @stesura/* dependencies) in
transpilePackages — see apps/testbed/next.config.mjs for a working list.
Next.js / SSR
StesuraEditor uses browser APIs and cannot be server-rendered. Import it
with dynamic, and keep any component that renders it a Client Component
("use client"):
import dynamic from "next/dynamic";
import { EditorLoadingState } from "@stesura/editor-react";
const StesuraEditor = dynamic(
() => import("@stesura/editor-react").then((m) => m.StesuraEditor),
{ ssr: false, loading: () => <EditorLoadingState /> }
);EditorLoadingState is SSR-safe and can be imported statically for the
fallback.
Editor fonts on cold loads (optional)
By default the editor injects its Google Fonts stylesheet at mount
(loadEditorFonts, called by the mount hooks). That works everywhere with zero
setup, but on a cold cache it serializes: JS boots → editor mounts → fonts
CSS downloads → font files download. The document’s text — usually the largest
paint — waits for the end of that chain, and so does the first pagination pass
(it gates on document.fonts.ready).
If first-load performance matters, render the stylesheet link yourself in your
SSR’d document head. loadEditorFonts detects it by id and steps aside:
// app/layout.tsx
import {
EDITOR_FONTS_LINK_ID,
EDITOR_FONTS_PRECONNECT_ORIGINS,
editorFontsStylesheetHref,
} from "@stesura/fonts";
// inside <html>, before <body> — React hoists these into <head>
{EDITOR_FONTS_PRECONNECT_ORIGINS.map((origin) => (
<link key={origin} rel="preconnect" href={origin} crossOrigin="anonymous" />
))}
<link
id={EDITOR_FONTS_LINK_ID}
rel="stylesheet"
precedence="default"
href={editorFontsStylesheetHref()}
/>The fonts CSS now downloads in parallel with your JS, and the font files fetch over a warm connection the moment the editor mounts. Warm-cache visits are unaffected either way (the files are cached for a year).
Deliberately not provided: font-file (.woff2) preload URLs — Google’s CDN
negotiates file URLs per user agent, so hardcoding them risks preloading files
the stylesheet never references. If you need the last few hundred ms on cold
loads, self-host your default document font (the Normal style’s family) under
a stable same-origin URL and preload that.
A minimal editor
useLocalEditor owns an in-memory EditorState — no persistence, no backend.
This runs as-is (live → ):
"use client";
import { EditorLoadingState } from "@stesura/editor-react";
import { useLocalEditor } from "@stesura/editor-react/hooks";
import { StesuraUserProvider } from "@stesura/editor-react/providers";
import dynamic from "next/dynamic";
const StesuraEditor = dynamic(
() => import("@stesura/editor-react").then((m) => m.StesuraEditor),
{ ssr: false, loading: () => <EditorLoadingState /> }
);
const currentUser = { id: "user-1", name: "Ada" };
const Editor = () => {
const { editorState, dispatch, pluginFactory, schema } = useLocalEditor();
return (
<div className="h-full w-full">
<StesuraEditor
state={editorState}
dispatchTransaction={dispatch}
schema={schema}
pluginFactory={pluginFactory}
/>
</div>
);
};
export default function Page() {
return (
<StesuraUserProvider currentUser={currentUser}>
<Editor />
</StesuraUserProvider>
);
}StesuraUserProvider is required: the mount hooks read currentUser from it
and throw without it.
useLocalEditor also accepts initialContent (HTML string, ProseMirror
Node or JSON), a custom schema, extraPlugins and a transactionModifier
(see the next section and the API reference). schema
and initialContent are read once at mount; to switch documents, remount with
a key. If you pass an editorId, give StesuraEditor the same value as
field.
HTML content is browser-only. HTML strings are parsed with the DOM, so they throw in server code (route handlers, server actions). To seed documents server-side, pass ProseMirror JSON, or build heading/paragraph content with
createDocContent(schema, blocks)from@stesura/core/helpers:import { createDocContent } from "@stesura/core/helpers"; const doc = createDocContent(schema, [ { type: "heading", level: 1, text: "Minutes" }, { type: "paragraph", text: "Attendees: …" }, ]);
Adding features
Features ship as package pairs: a plugins package (document behaviour) and a
-react package (the UI extension with its toolbar items, panels and
decorations). Add each feature in two places: extraPlugins on the mount
hook, uiExtensions on the editor.
"use client";
// StesuraEditor is the dynamic import from the minimal example.
import { useLocalEditor } from "@stesura/editor-react/hooks";
import { commentsPlugins } from "@stesura/comments";
import { commentsUiExtension } from "@stesura/comments-react";
import { paginationPlugins } from "@stesura/pagination";
import { paginationUiExtension } from "@stesura/pagination-react";
import { trackChangesPlugin, trackChangesTransactionModifier } from "@stesura/track-changes";
import { trackChangesUIExtension } from "@stesura/track-changes-react";
// Module scope — the array must be referentially stable across renders.
const uiExtensions = [
trackChangesUIExtension,
paginationUiExtension,
commentsUiExtension,
];
const Editor = () => {
const { editorState, dispatch, pluginFactory, schema } = useLocalEditor({
transactionModifier: trackChangesTransactionModifier,
extraPlugins: () => [
trackChangesPlugin(),
...paginationPlugins(),
...commentsPlugins(),
],
});
return (
<StesuraEditor
state={editorState}
dispatchTransaction={dispatch}
schema={schema}
pluginFactory={pluginFactory}
// The hook's modifier covers the main editor; this one covers sub-editors.
transactionModifier={trackChangesTransactionModifier}
uiExtensions={uiExtensions}
/>
);
};The same pattern covers references, proofread, DOCX import/export, and PDF export. See UI extensions for what an extension can contribute.
Users, mentions, uploads
StesuraUserProvider (from @stesura/editor-react/providers) supplies
identity to every editor in its subtree:
| Prop | Type | Purpose |
|---|---|---|
currentUser | { id, name?, avatar?, color? } | Author of comments and tracked changes, collab presence identity. |
resolveUsers | ({ userIds }) => Promise<{ name, avatar? }[]> | Resolves ids to display data (comments, mentions, cursors), index-aligned with userIds. |
resolveMentionSuggestions | ({ text }) => Promise<string[]> | User ids matching an @ mention query. Without it the mention dropdown never opens. |
Uploads go through the onFileUpload prop on StesuraEditor:
(file, type, contentType?) => Promise<string | undefined>, resolving to the
URL to embed. See File & Image Uploads.
Persistence and collaboration
Swap the mount hook; everything else stays:
| You want | Hook | Docs |
|---|---|---|
| In-memory (previews, forms, tests) | useLocalEditor | this page |
| Offline persistence, single browser | useLocalCollabEditor (IndexedDB + BroadcastChannel) | Local |
| Real-time multi-user | useCollabEditor + a ~10-line backend on your Postgres + Redis | Pitter Patter |
| Multi-document rooms | RoomProvider + useRoomManifest | Rooms |
Comments work at every rung: no-op locally,
LocalCommentProvider for IndexedDB, and server-backed threads under
useCollabEditor — the UI is identical.
For a complete, realistic reference — Supabase auth, per-room membership
enforced in the collab routes’ authorize, document dashboard, and the fully
featured editor — read apps/testbed in this repo.
Next steps
- Collaboration — backend comparison and architecture.
- Plugins — what the plugin factory composes.
- Customizing the Toolbar and Customizing the Menus.
- Extending the schema.
- Live examples — every mount running in the demo app.