Guides

Advanced editor in React

Mount the collaborative AdvancedEditor on React with the chrome components beside it, over the same backend as the Vue surface.

AdvancedEditor from @skaile/forge-editor/react/advanced is the React counterpart of the Vue AdvancedEditor: an EditorSurface over composeEditor of its content, interaction, ai, add, remove and backend props (defaults { content: 'rich', interaction: 'full', ai: true }, i.e. advancedEditor()), which lists every feature and integration (core-content, core-ui, media, transcript, composites, mentions, authors, comments, ai, collab, block-ui, composite-media, composite-ai, transcript-ai, themes) with its React binding, in the same schema order as the Vue surface. It uses no Vue, Nuxt UI or Nuxt module. The runnable reference is examples/react-advanced (bun run --cwd examples/react-advanced dev, port 3043): the React counterpart of the Nuxt document page. It reuses the Nuxt app's documents, session and collaboration socket through the Vite proxy (/api, /collab), so start the Nuxt app first (bun run dev, port 3056).

Setup is the simple React setup plus three things:

  • Styles. Replace the three library imports by one: @import "@skaile/forge-editor/react/styles/advanced.css"; after tailwindcss and tw-animate-css (it imports ui-vars.css, core.css, every feature sheet and themes.css last, plus the bridge that lets an Obsidian theme restyle the shadcn tokens). Keep the @source for the library's whole dist (src in this repository, as the example does) and the shadcn theme block; examples/react-advanced/src/index.css has a complete copy.
  • Notices. Create the context with createEditorContext({ notify: reactNotifier }) and render sonner's <Toaster /> once inside the provider (both exported by @skaile/forge-editor/react). Features send Notices to the surface's notifier (import problems, AI rate limits, comment errors, „Dokument gelöscht“ with undo in the tree); without <Toaster /> none is visible.
  • Dependencies. The simple setup's list plus sonner (the Toaster and the notice seam), yjs and @hocuspocus/provider for collaboration; use the exact version ranges of the example's package.json.

main.tsx:

examples/react-advanced/src/main.tsx
import lucide from '@iconify-json/lucide/icons.json'
import { addCollection } from '@iconify/react'
import { createHttpBackend } from '@skaile/forge-editor/adapters/http'
import { createEditorContext } from '@skaile/forge-editor/core'
import { EditorContextProvider, reactNotifier, Toaster } from '@skaile/forge-editor/react'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'
import './index.css'

// Preload the icon set: the page makes no request to the Iconify API.
addCollection(lucide)

// The host owns the context; the provider never disposes it. The backend is the HTTP one (`/api/editor` over the Nuxt host,
// the collaboration websocket `/collab/<id>`); the backend offers `settings`, so they sync with the host's account store,
// notices of app-level models become sonner toasts.
const context = createEditorContext({ backend: createHttpBackend(), notify: reactNotifier })

createRoot(document.getElementById('app')!).render(
  <StrictMode>
    <EditorContextProvider context={context}>
      <App />
      <Toaster />
    </EditorContextProvider>
  </StrictMode>,
)

The example passes createHttpBackend() so settings sync with the host's /api/editor/settings; pass createMemoryBackend() for local-only settings.

The page. The chrome components beside the editor take the editor from onReady and find its preset through it. AdvancedEditor takes the surface's props except preset, plus the composition props (content, interaction, ai, add, remove, backend, blockTypes, blockUi, coreContent, read once at mount); <AdvancedEditor remove={['tables']} ai={false} /> is the advanced editor without tables and AI:

import { AiSidebar } from '@skaile/forge-editor/ai/react'
import { CommentsMargin } from '@skaile/forge-editor/comments/react'
import { CompositeInspector } from '@skaile/forge-editor/composites/react'
import type { CollaborationSession } from '@skaile/forge-editor/core'
import { EditorSettingsModal, EditorToolbar } from '@skaile/forge-editor/react'
import { AdvancedEditor } from '@skaile/forge-editor/react/advanced'
import type { Editor } from '@tiptap/core'
import { useState } from 'react'

export function Page({ id, session }: { id: string, session: CollaborationSession }) {
  const [editor, setEditor] = useState<Editor>()
  const [scrollEl, setScrollEl] = useState<HTMLElement | null>(null)
  const [settingsOpen, setSettingsOpen] = useState(false)
  const [sidebarOpen, setSidebarOpen] = useState(false)
  const [commentsOpen, setCommentsOpen] = useState(false)

  return (
    <>
      <EditorToolbar editor={editor} />
      <main ref={setScrollEl} className="relative flex min-h-0 flex-1 overflow-auto">
        <AdvancedEditor
          key={id}
          collaboration={session}
          documentId={id}
          settingsUi="external"
          sidebarOpen={sidebarOpen}
          onSidebarOpenChange={setSidebarOpen}
          commentsOpen={commentsOpen}
          onCommentsOpenChange={setCommentsOpen}
          onReady={setEditor}
          onSettingsOpen={() => setSettingsOpen(true)}
        />
        <CommentsMargin editor={editor} container={scrollEl} open={commentsOpen} />
      </main>
      <CompositeInspector editor={editor} />
      <AiSidebar editor={editor} open={sidebarOpen} onOpenChange={setSidebarOpen} />
      <EditorSettingsModal editor={editor} open={settingsOpen} onOpenChange={setSettingsOpen} />
    </>
  )
}

Notes:

  • Collaboration. The session is { ydoc, provider, user } (CollaborationSession from @skaile/forge-editor/core): create the Y.Doc and the HocuspocusProvider in an effect (so StrictMode's double mount leaves exactly one provider), destroy the provider in its cleanup, and key the editor by document so a new document is a new Y.Doc (Collaboration on the client). Without collaboration pass value/onChange for a standalone editor (the collaboration and comments features then contribute nothing).
  • Beside the editor. EditorToolbar, EditorSettingsModal/EditorSettingsPanel, CommentsMargin, CompositeTable, CompositeInspector, AiSidebar, EditorOutline and DocumentTree are components you place; each takes the editor from onReady. useEditorUi(editor) returns the chrome of a ready editor (openSettings(tab?), toggleSidebar(), toggleComments(), commentsOpen, togglePalette(), paletteOpen) for your own buttons.
  • Own features. Pass them as add={[myFeature()]} (appended after the catalog), or build the preset for an EditorSurface yourself: advancedEditor({ add: [myFeature()] }), as in the Nuxt app's kitchen sink (app/pages/kitchen-sink/surface.vue).
  • The file browser is DocumentTree from @skaile/forge-editor/tree/react over a DocumentStore; the example wires it to the host's /api.
  • Backend. The same as for the Vue advanced editor: /collab/:id, the AI endpoints, assets, users/tags and themes of the Nuxt module and a host adapter (Nuxt module and EditorPorts). A React app with its own server brings the same routes.
Copyright © 2026