Skip to content

Getting Started

Installation

Install the package for your framework:

bash
npm install @editu/react
# or
pnpm add @editu/react
# or
yarn add @editu/react

Peer Dependencies

Each framework package requires its respective framework as a peer dependency:

  • @editu/react requires react@^19 and react-dom@^19

Quick Start

Use the Editu component:

tsx
import { Editu } from '@editu/react';
import '@editu/core/styles.css';

function App() {
  return <Editu placeholder="Type '/' for commands..." />;
}

The Editu component includes the editor, bubble menu, and slash command menu.

Import Styles

Import the default stylesheet in your application entry point:

typescript
import '@editu/core/styles.css';

This includes both CSS variables and component styles. For custom theming, see Theming.

Advanced Usage

To customize the editor, you can use individual components:

React

tsx
import { EdituEditor, EdituBubbleMenu, useEdituEditor } from '@editu/react';
import '@editu/core/styles.css';

function Editor() {
  const editor = useEdituEditor({
    placeholder: "Type '/' for commands...",
  });

  return (
    <div className="editor-container">
      <EdituEditor editor={editor} />
      {editor && <EdituBubbleMenu editor={editor} />}
    </div>
  );
}

export default Editor;

Toolbar

You can enable the built-in fixed toolbar for a traditional formatting bar above the editor:

tsx
<Editu showToolbar placeholder="Type '/' for commands..." />

The toolbar includes undo/redo, text formatting, headings, lists, and block actions by default. See the API reference for EdituToolbar (React) for customization options.

Working with Content

Initial Content

You can initialize the editor with Markdown or JSON format.

Using Markdown

You can initialize with Markdown:

tsx
import { Editu } from '@editu/react';

<Editu initialMarkdown="# Hello World\n\nStart editing..." />

Or with the hook/composable/rune:

tsx
const editor = useEdituEditor({
  initialMarkdown: '# Hello World\n\nStart editing...',
});

Using JSON

You can initialize with JSON format:

tsx
const editor = useEdituEditor({
  initialContent: {
    type: 'doc',
    content: [
      {
        type: 'heading',
        attrs: { level: 1 },
        content: [{ type: 'text', text: 'Hello World' }],
      },
      {
        type: 'paragraph',
        content: [{ type: 'text', text: 'Start editing...' }],
      },
    ],
  },
});

Getting Content

You can access the editor content in multiple formats:

typescript
// Get JSON content
const json = editor.getJSON();

// Get Markdown content
const markdown = editor.getMarkdown();

// Get HTML content
const html = editor.getHTML();

// Get plain text
const text = editor.getText();

Listening to Changes

tsx
const editor = useEdituEditor({
  onUpdate: ({ editor }) => {
    const content = editor.getJSON();
    console.log('Content updated:', content);
    // Save to your backend
  },
});

Syncing Markdown Content

For two-way Markdown synchronization, use the dedicated hooks/composables/runes:

tsx
import { useEdituEditor, useEdituMarkdown, EdituEditor } from '@editu/react';

function Editor() {
  const editor = useEdituEditor();
  const { markdown, setMarkdown, isPending } = useEdituMarkdown(() => editor);
  
  // markdown updates automatically when editor content changes
  // setMarkdown() updates editor content from markdown
  
  return (
    <div>
      <EdituEditor editor={editor} />
      <textarea 
        value={markdown} 
        onChange={(e) => setMarkdown(e.target.value)}
      />
      {isPending && <span>Syncing...</span>}
    </div>
  );
}

Markdown Flavor

Editu supports multiple Markdown output flavors. The flavor option controls how content is serialized (e.g., callout format, wiki link syntax). Input parsing is always tolerant and accepts all formats.

tsx
const editor = useEdituEditor({
  flavor: 'obsidian', // 'commonmark' | 'gfm' (default) | 'obsidian' | 'docusaurus'
});

See Features - Markdown Flavor Selection for details on each flavor.

Enabling Features

All features are enabled by default except collaboration, comment, mention, and wikiLink. You can disable specific features by setting them to false, or pass an options object to configure them:

tsx
const editor = useEdituEditor({
  features: {
    // All features below are enabled by default.
    // Set to false to disable, or pass options to configure.
    slashCommand: true,
    table: true,
    image: { onUpload: async (file) => 'https://example.com/image.png' },
    codeBlock: true,
    dragHandle: true,
    characterCount: true,
    textColor: true,
    taskList: true,
    link: true,
    markdown: true,
    mathematics: true,
    embed: true,
    details: true,
    diagram: true,
    wikiLink: true,

    // Opt-in: must be explicitly enabled
    comment: true,
    collaboration: true,
  },
});

See Features for detailed configuration of each feature.

Composition Patterns

Editu offers two composition patterns for integrating the editor into your application. Choose the one that best fits your needs.

Simple: All-in-One <Editu> Component

The <Editu> component bundles the editor, bubble menu, and slash command menu into a single component. This is the recommended approach for most use cases where you need a standard editor with minimal configuration.

When to use:

  • Quick setup with sensible defaults
  • Standard editor layout (editor + bubble menu)
  • Configuration via props without managing the editor instance directly
tsx
import { Editu } from '@editu/react';
import '@editu/core/styles.css';

function App() {
  return (
    <Editu
      initialMarkdown="# Hello World"
      placeholder="Start writing..."
      showToolbar
      features={{ image: { onUpload: uploadImage } }}
      onUpdate={({ editor }) => console.log(editor.getMarkdown())}
    />
  );
}

Advanced: Decomposed Components

For full control over layout and behavior, create the editor instance yourself and compose individual components. This pattern uses EdituProvider to share the editor context with child components.

When to use:

  • Custom layout (e.g., toolbar in a separate header, sidebar panels)
  • Multiple editors on the same page
  • Fine-grained control over which UI elements to render
  • Integrating editor state into your own components via context
tsx
import {
  EdituProvider,
  EdituEditor,
  EdituBubbleMenu,
  EdituToolbar,
  useEdituEditor,
} from '@editu/react';
import '@editu/core/styles.css';

function App() {
  const editor = useEdituEditor({
    placeholder: "Start writing...",
    features: { image: { onUpload: uploadImage } },
    onUpdate: ({ editor }) => console.log(editor.getMarkdown()),
  });

  return (
    <EdituProvider editor={editor}>
      <header>
        <EdituToolbar editor={editor} />
      </header>
      <main>
        <EdituEditor editor={editor} />
      </main>
      {editor && <EdituBubbleMenu editor={editor} />}
    </EdituProvider>
  );
}

TIP

Components inside EdituProvider can also access the editor via the useEdituContext hook without passing the editor prop explicitly. Passing the prop directly is recommended for clarity and type safety.

Image Upload

You can configure image uploads with a custom handler:

tsx
const editor = useEdituEditor({
  features: {
    image: {
      onUpload: async (file) => {
        // Upload to your server/CDN
        const formData = new FormData();
        formData.append('file', file);
        
        const response = await fetch('/api/upload', {
          method: 'POST',
          body: formData,
        });
        
        const { url } = await response.json();
        return url;
      },
      maxFileSize: 10 * 1024 * 1024, // 10MB
      allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
    },
  },
});

Dark Mode

Use EdituThemeProvider for theme support:

tsx
import { EdituThemeProvider, useEdituTheme } from '@editu/react';

function App() {
  return (
    <EdituThemeProvider defaultTheme="system" storageKey="my-theme">
      <Editor />
      <ThemeToggle />
    </EdituThemeProvider>
  );
}

function ThemeToggle() {
  const { resolvedTheme, setTheme } = useEdituTheme();
  
  return (
    <button onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}>
      {resolvedTheme === 'dark' ? '☀️' : '🌙'}
    </button>
  );
}

See Theming for more customization options.

Next Steps

Released under the MIT License.