---
title: "Quick start: React"
description: "Set up the simple editor in a Vite and React app: peers, Tailwind and shadcn tokens, EditorContextProvider, EditorSurface and SimpleEditor."
canonical_url: "https://skaile-ai.github.io/forge-editor/getting-started/quick-start-react"
---
# Quick start: React

> Set up the simple editor in a Vite and React app: peers, Tailwind and shadcn tokens, EditorContextProvider, EditorSurface and SimpleEditor.

The React layer (`@skaile/forge-editor/react`, shadcn/ui on Radix and Tailwind 4) renders the same framework-free core as the Vue layer: the simple editor, `EditorToolbar`, the settings panel and modal, and the `@`, `#` and `/` suggestion popups. It uses no Vue, Nuxt UI or Nuxt module. The runnable reference is [`examples/react-simple`](https://github.com/skaile-ai/forge-editor/tree/main/examples/react-simple); start it with `bun run --cwd examples/react-simple dev` (port 3041). It has the same sample picker, read-only toggle, host `/datum` command and JSON and Markdown panes as the Vue example, plus `@` mentions.

**More than the simple editor:** every advanced feature has a React binding as well (`./<x>/react`: media, transcript, Typed blocks, authors, comments, AI, collaboration, block UI, themes, the Block outline and the Document tree), and `composeEditor(options)` / `advancedEditor()` / `AdvancedEditor` from `@skaile/forge-editor/react/advanced` compose them all; see [Advanced editor in React](https://skaile-ai.github.io/forge-editor/guides/advanced-react). `coreContentFeature()`, `coreUiFeature()`, `mentionsFeature()` and the three integrations have no components and work in both layers; a React preset is `defineEditor({ features })` over features from `./<x>/react` and such component-free features.

## Setup

- `package.json` dependencies: `@skaile/forge-editor` (`workspace:*` inside this repository), `react`, `react-dom`, `@tiptap/core`, `@tiptap/pm`, `@tiptap/react`, `@iconify/react`, `@iconify-json/lucide`, `tailwindcss`, `tw-animate-css`, `yjs`, `@hocuspocus/provider` (the last two as in the [README](https://skaile-ai.github.io/forge-editor/getting-started/installation): source-shipped shared types reference them); dev dependencies `vite`, `@tailwindcss/vite`, `@vitejs/plugin-react`, `typescript`, `@types/react`, `@types/react-dom`. Use the exact version ranges of the example's [`package.json`](https://github.com/skaile-ai/forge-editor/blob/main/examples/react-simple/package.json).
- `index.html` with `<div id="app"></div>` and `<script type="module" src="/src/main.tsx"></script>`.
- `tsconfig.json` with `jsx: "react-jsx"`, `moduleResolution: "Bundler"`, `strict` and `include: ["src"]` (see the example's [`tsconfig.json`](https://github.com/skaile-ai/forge-editor/blob/main/examples/react-simple/tsconfig.json)).

`vite.config.ts`:

```ts
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({ plugins: [react(), tailwindcss()] })
```

`index.css` — Tailwind, the animation utilities the shadcn dialog, dropdown and tooltip use, the library sheets, and a `@source` so Tailwind generates the utility classes the library's React code uses (Tailwind skips `node_modules` unless told otherwise):

```css
@import "tailwindcss";
@import "tw-animate-css";
/* ui-vars.css maps the shadcn tokens (--primary, --background, …) to the --ui-* variables the feature sheets read. */
@import "@skaile/forge-editor/react/styles/ui-vars.css";
@import "@skaile/forge-editor/styles/core.css";
@import "@skaile/forge-editor/styles/mentions.css"; /* the sheet of each feature you use */

@source "../node_modules/@skaile/forge-editor/dist"; /* relative path from this CSS file to the installed package's dist */

/* The shadcn theme block follows: @custom-variant dark, @theme inline, :root and .dark tokens, @layer base.
   `bunx shadcn@latest init` generates it; examples/react-simple/src/index.css has a complete copy. */
```

The `@source` covers the whole `dist`, not just `dist/react`: the Features' own React components (`features/*/react`, e.g. the image studio dialog), the surfaces (`document`, `markdown`, `code`), the outline and the tree sit beside it, and a host that scans only `dist/react` renders them without their layout classes. In this repository the examples point at the sibling's `src` (`../../../packages/editor/src`) instead.

`main.tsx` — icons and the editor context. The toolbar and menus render `i-lucide-*` names through `@iconify/react`; preloading the collection means the page makes no request to the Iconify API. The host owns the context from `createEditorContext()` (`@skaile/forge-editor/core`) and every React component of the library reads it from `EditorContextProvider`; the provider never disposes it, call `context.dispose()` yourself when the context outlives your app. `createEditorContext` takes the same options as the Vue `createEditor` (the default backend syncs the settings with `GET`/`PUT /api/editor/settings`; `{ backend: createMemoryBackend() }` keeps them local, `{ backend: createHttpBackend({ fetch }) }` brings your own HTTP client):

```tsx
import lucide from '@iconify-json/lucide/icons.json'
import { addCollection } from '@iconify/react'
import { createEditorContext } from '@skaile/forge-editor/core'
import { EditorContextProvider } from '@skaile/forge-editor/react'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'
import './index.css'

addCollection(lucide)
const context = createEditorContext()

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

Notices of app-level models (for example the AI model profiles) go through the context's notifier. The simple editor raises none, so `createEditorContext()` is enough here; hosts of features pass `createEditorContext({ notify: reactNotifier })` and mount sonner's `Toaster` once (both from `@skaile/forge-editor/react`, see [Advanced editor in React](https://skaile-ai.github.io/forge-editor/guides/advanced-react)).

## Mount the editor

`EditorSurface` takes a preset; `EditorToolbar` is a sibling that works on the editor you pass it (from `onReady`), never on a global active editor. The toolbar renders the groups of the preset registered for that editor; without an editor it renders none.

```tsx
import { coreContentFeature, coreUiFeature, defineEditor } from '@skaile/forge-editor/core'
import { mentionsFeature } from '@skaile/forge-editor/mentions'
import { EditorSurface, EditorToolbar } from '@skaile/forge-editor/react'
import type { Editor, JSONContent } from '@tiptap/core'
import { useState } from 'react'

// Module constants: the preset, `metadata`, `abilities`, `extensions` and `commands` are read once at mount.
const preset = defineEditor({ features: [coreContentFeature(), coreUiFeature(), mentionsFeature()] })
const metadata = { mentions: [{ id: 'u1', label: 'Ada Lovelace' }, { id: 'u2', label: 'Alan Turing' }] }

export function App() {
  const [editor, setEditor] = useState<Editor | null>(null)
  const [content, setContent] = useState<JSONContent>({ type: 'doc', content: [{ type: 'paragraph' }] })

  return (
    <>
      <EditorToolbar editor={editor ?? undefined} />
      <EditorSurface
        preset={preset}
        metadata={metadata}
        value={content}
        onChange={setContent}
        onReady={setEditor}
        onContentError={error => console.warn(error.message)}
      />
    </>
  )
}
```

Typing `@Ad` opens the mention popup with the `mentions` list. A list the host does not supply (here `hashtags`) comes from `/api/editor/tags` through the context's HTTP client, and only once the `#` popup is used. The simple editor's `/` menu lists the core blocks; pass `commands={defaults => [...defaults, ...]}` (a module function) to add own entries.

## The one-liner: `SimpleEditor`

Without own features, `SimpleEditor` from `@skaile/forge-editor/react/simple` builds the preset itself (`core-content` + `core-ui`, plus the features in its `add` prop) on an `EditorSurface`. The toolbar takes its groups from the editor it is given; the ref exposes `{ editor, openSettings, preset }`:

```tsx
import { EditorToolbar } from '@skaile/forge-editor/react'
import { SimpleEditor } from '@skaile/forge-editor/react/simple'
import type { Editor } from '@tiptap/core'
import { useState } from 'react'

export function App() {
  const [editor, setEditor] = useState<Editor | null>(null)
  return (
    <>
      <EditorToolbar editor={editor ?? undefined} />
      <SimpleEditor onReady={setEditor} />
    </>
  )
}
```

Try the React example here:

<demo-frame name="react-simple">



</demo-frame>

## Next steps

The [tutorial](https://skaile-ai.github.io/forge-editor/tutorial/react-surface) shows the same editor on the React surface. The advanced editor on React is covered in [Advanced editor in React](https://skaile-ai.github.io/forge-editor/guides/advanced-react); every entry is listed in the [exports reference](https://skaile-ai.github.io/forge-editor/reference/exports).


## Sitemap

See the full [sitemap](https://skaile-ai.github.io/forge-editor/sitemap.md) for all pages.
