blokhaus

EditorRoot

The root editor component that wraps Lexical's LexicalComposer.

EditorRoot is the foundational component of every Blokhaus editor instance. It wraps Lexical's LexicalComposer with all registered custom nodes, sets up the rich text plugin, history tracking, and auto-focus. All other Blokhaus components and plugins must be rendered as children of EditorRoot.

Import

import { EditorRoot } from "@blokhaus/core";

Props

PropTypeDefaultDescription
namespacestringrequiredUnique identifier for multi-editor isolation. Each editor on the page must have a distinct namespace.
initialStatestring | nullundefinedSerialized Lexical JSON state. Pass null or omit for a blank editor.
childrenReact.ReactNodeundefinedPlugin and UI components (FloatingToolbar, SlashMenu, etc.)
classNamestringundefinedCSS class applied to the editor container <div>.
dir"ltr" | "rtl" | "auto"undefinedDocument-level text direction. When set to "ltr" or "rtl", the root node direction is set on mount. Omit for per-paragraph auto-detection.
placeholderstring"Start writing..."Placeholder text shown when the editor is empty.
editablebooleantrueWhether the editor content is editable. Set to false for read-only mode.
autoFocusbooleantrueWhether the editor auto-focuses on mount.
onError(error: Error) => voidconsole.error + throwCustom error handler for Lexical errors. The default handler logs to the console and re-throws.

Basic usage

app/editor/page.tsx
"use client";

import {
  EditorRoot,
  FloatingToolbar,
  SlashMenu,
  InputRulePlugin,
} from "@blokhaus/core";

export default function EditorPage() {
  return (
    <EditorRoot
      namespace="my-editor"
      className="relative min-h-[400px] p-4 border rounded-lg"
      placeholder="Start writing something amazing..."
    >
      <FloatingToolbar />
      <SlashMenu />
      <InputRulePlugin />
    </EditorRoot>
  );
}

With plugins as children

Every feature in Blokhaus is a composable plugin passed as a React child. There is no prop explosion -- you only include what you need:

app/editor/page.tsx
"use client";

import {
  EditorRoot,
  FloatingToolbar,
  SlashMenu,
  OverlayPortal,
  MobileToolbar,
  InputRulePlugin,
  ImagePlugin,
  PastePlugin,
  LinkPlugin,
  ListPlugin,
  ColorPlugin,
  BlockSelectionPlugin,
  useEditorState,
} from "@blokhaus/core";

function StateTracker() {
  const { serializedState } = useEditorState({
    debounceMs: 300,
    onChange: (json) => {
      // Persist to your backend
      fetch("/api/save", {
        method: "POST",
        body: json,
      });
    },
  });
  return null;
}

const uploadHandler = async (file: File): Promise<string> => {
  const formData = new FormData();
  formData.append("file", file);
  const res = await fetch("/api/upload", { method: "POST", body: formData });
  const { url } = await res.json();
  return url;
};

export default function EditorPage() {
  return (
    <EditorRoot
      namespace="my-editor"
      className="relative min-h-[500px] p-4 border rounded-lg"
    >
      <StateTracker />

      {/* UI Components */}
      <FloatingToolbar />
      <SlashMenu />
      <OverlayPortal namespace="my-editor" />
      <MobileToolbar />

      {/* Plugins */}
      <InputRulePlugin />
      <ImagePlugin uploadHandler={uploadHandler} />
      <PastePlugin />
      <LinkPlugin />
      <ListPlugin />
      <ColorPlugin />
      <BlockSelectionPlugin />
    </EditorRoot>
  );
}

Multi-editor example

Multiple EditorRoot instances can coexist on the same page. Each must have a unique namespace:

app/document/page.tsx
"use client";

import { EditorRoot, FloatingToolbar, InputRulePlugin } from "@blokhaus/core";

export default function DocumentPage() {
  return (
    <div className="space-y-8">
      {/* Title editor */}
      <EditorRoot
        namespace="title-editor"
        className="text-3xl font-bold p-2 border-b"
        placeholder="Untitled"
        autoFocus={true}
      >
        <InputRulePlugin />
      </EditorRoot>

      {/* Body editor */}
      <EditorRoot
        namespace="body-editor"
        className="relative min-h-[400px] p-4"
        placeholder="Start writing..."
        autoFocus={false}
      >
        <FloatingToolbar />
        <InputRulePlugin />
      </EditorRoot>
    </div>
  );
}

Each editor maintains fully isolated state. Typing in one editor does not affect the other. Keyboard shortcuts (e.g., Cmd+B) are automatically scoped to the editor that has focus via Lexical's COMMAND_PRIORITY_EDITOR.

Read-only mode

Set editable={false} to render the editor in read-only mode. This is useful for displaying saved content without allowing modifications:

<EditorRoot
  namespace="viewer"
  initialState={savedJsonState}
  editable={false}
  autoFocus={false}
  className="prose"
/>

Restoring saved state

Pass a serialized Lexical JSON string to initialState to restore previously saved content:

const savedState = localStorage.getItem("editor-state");

<EditorRoot namespace="my-editor" initialState={savedState}>
  {/* plugins */}
</EditorRoot>;

RTL support

For right-to-left languages, set the dir prop:

<EditorRoot namespace="rtl-editor" dir="rtl" placeholder="...ابدأ الكتابة">
  <FloatingToolbar />
  <InputRulePlugin />
</EditorRoot>

When dir is set to "ltr" or "rtl", the root node's direction is set once on mount. Per-paragraph direction can still be changed via the floating toolbar's direction toggle.

Notes

  • EditorRoot is a client component ('use client'). It cannot be used as a Server Component.
  • It wraps LexicalComposer with the full ALL_NODES registry, which includes all custom Blokhaus nodes (ImageNode, CodeBlockNode, MentionNode, etc.).
  • The Lexical theme (blokhausTheme) is applied automatically for consistent CSS class names on editor elements.
  • The namespace prop is passed through to LexicalComposer and is also rendered as a data-namespace attribute on the wrapper <div> for DOM identification.
  • The editor container includes data-blokhaus-root for use by OverlayPortal and other components that need to find the editor boundary.
  • Custom error handling can be configured via onError. In production, you should replace the default handler with your error tracking service.