Skip to main content
The @lexical/plain-text package provides a lightweight editor for plain text input without formatting, ideal for comments, code editors, or simple text fields.

Setup

1
Install the Package
2
npm install @lexical/plain-text
3
Register Plain Text Commands
4
import { createEditor } from 'lexical';
import { registerPlainText } from '@lexical/plain-text';

const editor = createEditor({
  namespace: 'PlainTextEditor',
  onError: (error) => console.error(error),
});

const unregister = registerPlainText(editor);

// Cleanup when done
unregister();

React Integration

Use PlainTextPlugin for React applications:
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';

function Editor() {
  const initialConfig = {
    namespace: 'PlainTextEditor',
    onError: (error: Error) => {
      console.error(error);
    },
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <PlainTextPlugin
        contentEditable={<ContentEditable className="editor-input" />}
        placeholder={<div className="editor-placeholder">Enter text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
    </LexicalComposer>
  );
}
See packages/lexical-react/src/LexicalPlainTextPlugin.tsx for the plugin implementation.

Features

Plain text mode provides:
  • Text Input: Basic text editing without formatting
  • Line Breaks: Support for multi-line text
  • Copy/Paste: Plain text clipboard operations
  • Keyboard Navigation: Arrow keys, Home, End, etc.
  • Selection: Text selection and manipulation
  • Undo/Redo: History support

Supported Commands

Plain text plugin handles these commands:

Text Insertion

import { CONTROLLED_TEXT_INSERTION_COMMAND } from 'lexical';

// Insert text
editor.dispatchCommand(CONTROLLED_TEXT_INSERTION_COMMAND, 'Hello');

// Insert with InputEvent
editor.dispatchCommand(CONTROLLED_TEXT_INSERTION_COMMAND, inputEvent);

Line Breaks

import { INSERT_LINE_BREAK_COMMAND, INSERT_PARAGRAPH_COMMAND } from 'lexical';

// Insert line break (Shift + Enter)
editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false);

// Insert paragraph (Enter)
editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);

Deletion

import {
  DELETE_CHARACTER_COMMAND,
  DELETE_WORD_COMMAND,
  DELETE_LINE_COMMAND,
  REMOVE_TEXT_COMMAND,
} from 'lexical';

// Delete character (Backspace/Delete)
editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true); // backward
editor.dispatchCommand(DELETE_CHARACTER_COMMAND, false); // forward

// Delete word (Ctrl+Backspace/Delete)
editor.dispatchCommand(DELETE_WORD_COMMAND, true);

// Delete line
editor.dispatchCommand(DELETE_LINE_COMMAND, true);

// Remove selected text
editor.dispatchCommand(REMOVE_TEXT_COMMAND, null);

Clipboard

import { COPY_COMMAND, CUT_COMMAND, PASTE_COMMAND } from 'lexical';

// These are handled automatically by the plugin
See packages/lexical-plain-text/src/index.ts:118-423 for command implementations.

Keyboard Shortcuts

Plain text plugin supports:
  • Enter: Insert line break
  • Backspace: Delete backward
  • Delete: Delete forward
  • Ctrl/Cmd + A: Select all
  • Ctrl/Cmd + C: Copy
  • Ctrl/Cmd + X: Cut
  • Ctrl/Cmd + V: Paste
  • Ctrl/Cmd + Z: Undo
  • Ctrl/Cmd + Shift + Z: Redo
  • Arrow Keys: Navigate text
  • Home/End: Navigate to line start/end

Getting Text Content

Read Text

import { $getRoot } from 'lexical';

editor.getEditorState().read(() => {
  const root = $getRoot();
  const text = root.getTextContent();
  console.log(text);
});

Listen to Changes

import { $getRoot } from 'lexical';

editor.registerUpdateListener(({ editorState }) => {
  editorState.read(() => {
    const root = $getRoot();
    const text = root.getTextContent();
    console.log('Text changed:', text);
  });
});

Setting Text Content

import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical';

editor.update(() => {
  const root = $getRoot();
  root.clear();
  
  const paragraph = $createParagraphNode();
  const text = $createTextNode('Hello, world!');
  paragraph.append(text);
  root.append(paragraph);
});

Validation Example

Limit text length:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect, useState } from 'react';
import { $getRoot } from 'lexical';

function CharacterLimitPlugin({ maxLength }: { maxLength: number }) {
  const [editor] = useLexicalComposerContext();
  const [characterCount, setCharacterCount] = useState(0);

  useEffect(() => {
    return editor.registerUpdateListener(({ editorState }) => {
      editorState.read(() => {
        const root = $getRoot();
        const text = root.getTextContent();
        const count = text.length;
        
        setCharacterCount(count);
        
        if (count > maxLength) {
          // Truncate text
          editor.update(() => {
            const root = $getRoot();
            const text = root.getTextContent();
            const truncated = text.substring(0, maxLength);
            // Set truncated text...
          });
        }
      });
    });
  }, [editor, maxLength]);

  return (
    <div className="character-count">
      {characterCount} / {maxLength}
    </div>
  );
}

Textarea Replacement

Replace a textarea with a plain text editor:
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';

function FormField({ name, onValueChange }: Props) {
  return (
    <LexicalComposer initialConfig={config}>
      <PlainTextPlugin
        contentEditable={
          <ContentEditable
            className="form-input"
            aria-label={name}
          />
        }
        placeholder={<div>Enter {name}...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <ValuePlugin onChange={onValueChange} />
    </LexicalComposer>
  );
}

function ValuePlugin({ onChange }: { onChange: (value: string) => void }) {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    return editor.registerUpdateListener(({ editorState }) => {
      editorState.read(() => {
        const text = $getRoot().getTextContent();
        onChange(text);
      });
    });
  }, [editor, onChange]);

  return null;
}

Code Editor Example

Build a simple code editor:
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { CodeHighlightPlugin } from './CodeHighlightPlugin';

function CodeEditor() {
  const config = {
    namespace: 'CodeEditor',
    theme: {
      text: {
        code: 'code-text',
      },
    },
    onError: console.error,
  };

  return (
    <LexicalComposer initialConfig={config}>
      <div className="code-editor">
        <PlainTextPlugin
          contentEditable={
            <ContentEditable className="code-input" spellCheck={false} />
          }
          placeholder={<div className="code-placeholder">// code here</div>}
          ErrorBoundary={LexicalErrorBoundary}
        />
        <CodeHighlightPlugin />
      </div>
    </LexicalComposer>
  );
}

Plain Text vs Rich Text

FeaturePlain TextRich Text
Text FormattingNoneBold, Italic, etc.
HeadingsNoYes
ListsNoYes
LinksNoYes
ImagesNoYes
File SizeSmallerLarger
ComplexitySimpleComplex
Use CasesComments, CodeDocuments, Articles

Styling

Style the plain text editor:
.editor-input {
  min-height: 100px;
  padding: 10px;
  font-family: monospace;
  font-size: 14px;
  line-height: 1.5;
  outline: none;
  white-space: pre-wrap;
  word-wrap: break-word;
}

.editor-placeholder {
  position: absolute;
  top: 10px;
  left: 10px;
  color: #999;
  pointer-events: none;
}

.editor-input:focus {
  outline: none;
}

Differences from Textarea

Advantages over <textarea>:
  • Extensibility: Add plugins for autocomplete, mentions, etc.
  • Styling: Rich CSS styling capabilities
  • Performance: Better for large text content
  • Features: Built-in undo/redo, history
  • Mobile: Better mobile editing experience
Disadvantages:
  • Complexity: More setup than textarea
  • Bundle Size: Larger JavaScript bundle
  • Accessibility: Need to ensure proper ARIA attributes

Best Practices

  • Spell Check: Set spellCheck attribute on ContentEditable
  • Accessibility: Provide proper aria-label attributes
  • Performance: Use registerUpdateListener with debouncing for large text
  • Validation: Implement validation in update listeners
  • Mobile: Test on mobile devices for touch input
  • Cleanup: Always cleanup listeners on unmount

See Also

Build docs developers (and LLMs) love