Skip to main content

Overview

The Yoopta Editor is the main component that orchestrates all plugins, marks, and UI components. You create an editor instance with createYooptaEditor() (passing plugins and optional marks/value there), then render <YooptaEditor editor={editor} onChange={...} />. Content is stored on the editor instance and synced via onChange.

Creating an Editor Instance

Use createYooptaEditor() with an options object. Plugins are required; marks and initial value are optional:
import { createYooptaEditor } from '@yoopta/editor';
import Paragraph from '@yoopta/paragraph';

const editor = useMemo(
  () =>
    createYooptaEditor({
      plugins: [Paragraph],
      marks: [], // optional, e.g. from @yoopta/marks
      value: undefined, // optional initial content
      id: undefined, // optional editor ID
      readOnly: false,
    }),
  [],
);
Always wrap createYooptaEditor() in useMemo (with stable plugins/marks) to prevent recreating the editor on every render.
Plugins and marks are configured at creation time, not on the <YooptaEditor> component. Value is managed via the editor instance and onChange.

YooptaEditor Component Props

The <YooptaEditor> component accepts:

Required

editor
YooEditor
required
The editor instance from createYooptaEditor()

Optional

onChange
(value: YooptaContentValue, options: YooptaOnChangeOptions) => void
Called when content changes. Use it to sync value to your state (e.g. for saving or controlled usage).
onPathChange
(path: YooptaPath) => void
Called when the selection path changes (current block, selected blocks, etc.).
autoFocus
boolean
Whether to focus the editor on mount.
placeholder
string
Global placeholder text shown when the editor has a single empty block. If a plugin defines its own element-level placeholder, the element-level placeholder takes priority in multi-block editors. See Placeholders for details.
className
string
CSS class for the editor wrapper.
style
CSSProperties
Inline styles for the editor wrapper.
children
ReactNode
UI components to render inside the editor (e.g. toolbar, slash menu, block actions).
renderBlock
(props: RenderBlockProps) => ReactNode
Custom wrapper for each block (e.g. for drag-and-drop).

Basic Example

import { useMemo, useState } from 'react';
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import Paragraph from '@yoopta/paragraph';

const plugins = [Paragraph];

export default function MyEditor() {
  const editor = useMemo(
    () =>
      createYooptaEditor({
        plugins,
        value: undefined,
      }),
    [],
  );

  const [value, setValue] = useState(editor.children);

  const handleChange = (newValue, options) => {
    setValue(newValue);
    console.log('Operations:', options.operations);
  };

  return (
    <YooptaEditor
      editor={editor}
      onChange={handleChange}
      placeholder="Start typing..."
      autoFocus
    />
  );
}

Placeholders

Yoopta supports placeholders at two levels that work together:
  • Editor-level — a global fallback placeholder passed to <YooptaEditor placeholder="..." />
  • Element-level — per-plugin placeholders set via .extend() on each element type

How resolution works

When a block is focused and its element is empty, the editor resolves which placeholder to display:
  1. Check the focused element’s type (e.g. heading-one, step-list-item-heading)
  2. If that element has a placeholder defined — use it
  3. Otherwise, fall back to the editor-level placeholder prop
This means element-level placeholders always take priority over the global one.

Editor-level placeholder

<YooptaEditor
  editor={editor}
  placeholder="Type / to open menu..."
/>

Element-level placeholder

Set placeholders per element type using .extend():
import { HeadingOne } from '@yoopta/headings';
import Paragraph from '@yoopta/paragraph';

const plugins = [
  Paragraph.extend({
    elements: {
      paragraph: {
        placeholder: 'Type something...',
      },
    },
  }),
  HeadingOne.extend({
    elements: {
      'heading-one': {
        placeholder: 'Heading 1',
      },
    },
  }),
];

Nested plugins

For plugins with multiple nested elements (e.g. Steps, Accordion), you can set a different placeholder for each element:
import Steps from '@yoopta/steps';

const plugins = [
  Steps.extend({
    elements: {
      'step-list-item-heading': {
        placeholder: 'Step title',
      },
      'step-list-item-content': {
        placeholder: 'Describe this step...',
      },
    },
  }),
];
Each nested element resolves its own placeholder independently — the heading shows “Step title” while the content area shows “Describe this step…”.

Styling

Placeholders are rendered via the .yoopta-placeholder CSS class and data-placeholder attribute. If you use a Yoopta theme (e.g. @yoopta/themes-shadcn), placeholder styles are included. For headless usage, add your own styles:
.yoopta-placeholder {
  position: relative;
}

.yoopta-placeholder::before {
  content: attr(data-placeholder);
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  opacity: 0.4;
  white-space: nowrap;
  user-select: none;
}

Content Value Structure

Content is a record of block ID → block data. Type names use PascalCase for block types (e.g. Paragraph), and kebab-case for element types inside value.
type YooptaContentValue = Record<string, YooptaBlockData>;

type YooptaBlockData = {
  id: string;
  type: string; // e.g. 'Paragraph', 'HeadingOne'
  meta: {
    order: number;
    depth: number;
    align?: 'left' | 'center' | 'right';
  };
  value: SlateElement[]; // Slate nodes (paragraph, heading-one, etc.)
};

Example

{
  "block-1": {
    "id": "block-1",
    "type": "Paragraph",
    "meta": { "order": 0, "depth": 0 },
    "value": [
      {
        "id": "el-1",
        "type": "paragraph",
        "children": [{ "text": "Hello World!" }]
      }
    ]
  }
}

onChange Options

The onChange callback receives the new content and an options object:
type YooptaOnChangeOptions = {
  operations: YooptaOperation[]; // List of operations that caused the change
};
Use options.operations to know what changed (insert, update, delete, move, etc.) without diffing the whole value.

Styling the Editor

Using className

<YooptaEditor editor={editor} onChange={onChange} className="my-editor" />
.my-editor {
  max-width: 800px;
  margin: 0 auto;
  padding: 40px 20px;
  min-height: 500px;
}

Using style

<YooptaEditor
  editor={editor}
  onChange={onChange}
  style={{
    maxWidth: '800px',
    margin: '0 auto',
    padding: '40px 20px',
    minHeight: '500px',
  }}
/>

Multiple Editors

Each editor instance is independent. Create separate instances with createYooptaEditor() and separate state for each:
function MultipleEditors() {
  const editor1 = useMemo(() => createYooptaEditor({ plugins }), []);
  const editor2 = useMemo(() => createYooptaEditor({ plugins }), []);

  const [value1, setValue1] = useState(editor1.children);
  const [value2, setValue2] = useState(editor2.children);

  return (
    <>
      <YooptaEditor editor={editor1} onChange={setValue1} />
      <YooptaEditor editor={editor2} onChange={setValue2} />
    </>
  );
}

Next Steps