blokhaus

DirectionPlugin

Per-block text direction (LTR/RTL).

Import

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

Overview

The DirectionPlugin provides per-block text direction control, supporting both left-to-right (LTR) and right-to-left (RTL) content. It registers the SET_BLOCK_DIRECTION_COMMAND and includes automatic direction detection for new paragraphs based on the direction of the previous sibling block. The plugin is essential for multilingual editors that need to support Arabic, Hebrew, Persian, Urdu, and other RTL scripts alongside LTR content.

Props

The DirectionPlugin accepts no props. Direction handling is automatic once the plugin is mounted.

Usage

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

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

export default function EditorPage() {
  return (
    <EditorRoot namespace="my-editor">
      <DirectionPlugin />
    </EditorRoot>
  );
}

Registered commands

CommandPayloadDescription
SET_BLOCK_DIRECTION_COMMAND'ltr' | 'rtl'Sets the text direction on the block containing the current selection.

Dispatching programmatically

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

// Set the current block to RTL
editor.dispatchCommand(SET_BLOCK_DIRECTION_COMMAND, "rtl");

// Set the current block to LTR
editor.dispatchCommand(SET_BLOCK_DIRECTION_COMMAND, "ltr");

Auto-detection

The plugin automatically detects the appropriate text direction for new paragraphs using two mechanisms:

1. Previous sibling inheritance

When a new paragraph is created (e.g., by pressing Enter), the plugin checks the direction of the previous sibling block. If the previous block has an explicit direction set, the new paragraph inherits that direction. This provides a seamless writing experience when composing long passages in a single direction.

2. Content-based RTL detection

When the user begins typing in a new paragraph, the plugin examines the first strong directional character in the text. If it is an RTL character, the block direction is automatically set to RTL. If it is an LTR character, the direction is set to LTR.

The RTL detection uses a regex pattern that matches characters from RTL Unicode ranges:

// Simplified version of the RTL detection regex
const RTL_REGEX =
  /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0780-\u07BF\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;

This covers:

  • Hebrew (U+0590 - U+05FF)
  • Arabic (U+0600 - U+06FF)
  • Syriac (U+0700 - U+074F)
  • Thaana (U+0780 - U+07BF)
  • Arabic Extended-A (U+08A0 - U+08FF)
  • Arabic Presentation Forms (U+FB50 - U+FDFF, U+FE70 - U+FEFF)

How direction is stored

Direction is stored as a dir attribute on block-level nodes in the Lexical AST. The serialized JSON includes the direction:

{
  "type": "paragraph",
  "direction": "rtl",
  "children": [{ "type": "text", "text": "مرحبا بالعالم" }]
}

When no explicit direction is set, the direction field is null and the block inherits the document's default direction (typically LTR).

Building a direction toggle

The plugin provides the command but not the UI. Here is an example of a direction toggle button:

components/DirectionToggle.tsx
"use client";

import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { SET_BLOCK_DIRECTION_COMMAND } from "@blokhaus/core";
import { $getSelection, $isRangeSelection } from "lexical";
import { useState, useEffect } from "react";

export function DirectionToggle() {
  const [editor] = useLexicalComposerContext();
  const [currentDirection, setCurrentDirection] = useState<"ltr" | "rtl">(
    "ltr",
  );

  useEffect(() => {
    return editor.registerUpdateListener(({ editorState }) => {
      editorState.read(() => {
        const selection = $getSelection();
        if (!$isRangeSelection(selection)) return;

        const node = selection.anchor.getNode();
        const topLevel = node.getTopLevelElement();
        if (topLevel) {
          setCurrentDirection(topLevel.getDirection() ?? "ltr");
        }
      });
    });
  }, [editor]);

  const toggleDirection = () => {
    const newDirection = currentDirection === "ltr" ? "rtl" : "ltr";
    editor.dispatchCommand(SET_BLOCK_DIRECTION_COMMAND, newDirection);
  };

  return (
    <button
      onClick={toggleDirection}
      className="px-2 py-1 text-sm border rounded"
      aria-label={`Switch to ${currentDirection === "ltr" ? "RTL" : "LTR"}`}
    >
      {currentDirection === "ltr" ? "LTR" : "RTL"}
    </button>
  );
}

Mixed-direction documents

A single document can contain blocks with different directions. Each block maintains its own direction independently:

[LTR] This is a left-to-right paragraph.
[RTL] هذه فقرة من اليمين إلى اليسار.
[LTR] Back to left-to-right content.
[RTL] עברית מימין לשמאל.

The direction is applied via the HTML dir attribute on the block's DOM element, which controls text alignment, punctuation placement, and cursor movement direction.

Keyboard shortcuts

The plugin does not register any keyboard shortcuts by default. If you want to add shortcuts for direction toggling, register them in a custom plugin:

import { COMMAND_PRIORITY_EDITOR, KEY_DOWN_COMMAND } from "lexical";
import { SET_BLOCK_DIRECTION_COMMAND } from "@blokhaus/core";

editor.registerCommand(
  KEY_DOWN_COMMAND,
  (event: KeyboardEvent) => {
    // Cmd+Shift+L for LTR, Cmd+Shift+R for RTL
    if (event.metaKey && event.shiftKey) {
      if (event.key === "l") {
        editor.dispatchCommand(SET_BLOCK_DIRECTION_COMMAND, "ltr");
        return true;
      }
      if (event.key === "r") {
        editor.dispatchCommand(SET_BLOCK_DIRECTION_COMMAND, "rtl");
        return true;
      }
    }
    return false;
  },
  COMMAND_PRIORITY_EDITOR,
);

Auto-detection only runs when the user types the first character in an empty paragraph. Once a direction is set (either manually or via auto-detection), it persists until explicitly changed. Editing existing text does not re-trigger auto-detection.

The direction is a block-level property. Inline text within a block always follows the block's direction. Mixed inline direction (e.g., an RTL word in an LTR paragraph) is handled by the browser's Unicode Bidirectional Algorithm (UBA) automatically -- no plugin intervention is needed.