Build a Custom Draft.js Comment Box with Length Limit, Mentions, and Links

This tutorial introduces Draft.js core concepts—EditorState, Entity, SelectionState, and CompositeDecorator—and walks through building a custom comment box in React that enforces a 200‑character limit, highlights @mentions, and supports link insertion using immutable state updates.

Qingyun Technology Community
Qingyun Technology Community
Qingyun Technology Community
Build a Custom Draft.js Comment Box with Length Limit, Mentions, and Links

Draft.js Overview

Draft.js is a React rich‑text editor framework that provides APIs such as EditorState, Entity, SelectionState, and CompositeDecorator, allowing developers to build custom editors.

EditorState

EditorState is the top‑level immutable state object representing the whole editor, including ContentState, SelectionState, decorators, undo/redo stack, and change type.

Current content (ContentState)

Current selection (SelectionState)

Decorator

Undo/redo stack

EditorChangeType

Because Draft.js works with immutable data, any modification creates a new EditorState instance.

Entity

Entity describes a piece of text with metadata, enabling features such as links, mentions, and embedded content.

{
    type: 'string',
    // entity type, e.g. 'LINK', 'TOKEN', 'PHOTO', 'IMAGE'
    mutability: 'MUTABLE' | 'IMMUTABLE' | 'SEGMENTED',
    // mutability controls how the text can be edited
    data: 'object',
    // custom metadata
}

Mutability values:

IMMUTABLE – the whole entity is removed or cannot be edited.

MUTABLE – the text inside can be edited (e.g., a link).

SEGMENTED – similar to immutable but allows partial deletion.

SelectionState

SelectionState represents the range of text selected in the editor, defined by an anchor (start) and a focus (end). The relative positions determine the direction of selection.

CompositeDecorator

CompositeDecorator scans ContentBlocks, finds matches based on a strategy, and renders them with a custom React component.

Implementing a Comment Box

Requirements: limit input to 200 characters, highlight @mentions, and insert links.

Length Restriction

Use handleBeforeInput and handlePastedText to block input when the current length plus the new characters would exceed the limit. The functions consider the length of selected text that will be replaced.

const MAX_LENGTH = 200;
// handleBeforeInput implementation …
// handlePastedText implementation …

Store the current length in state inside handleEditorChange to display a “current/maximum” counter.

@Mention Highlighting

Create a CompositeDecorator with a regex /@[\w]+/g that wraps matched text in a span with a special class.

const HANDLE_REGEX = /@[\w]+/g;
const compositeDecorator = new CompositeDecorator([
  {
    strategy: (contentBlock, callback) => {
      const text = contentBlock.getText();
      let matchArr, start;
      while ((matchArr = HANDLE_REGEX.exec(text)) !== null) {
        start = matchArr.index;
        callback(start, start + matchArr[0].length);
      }
    },
    component: props => <span className="mention">{props.children}</span>
  }
]);

Link Insertion

Use Entity with type LINK (immutable) and the Modifier API to insert or replace text, then force the selection to the end of the inserted link.

const insertEntity = (entityData) => {
  let contentState = editorState.getCurrentContent();
  contentState = contentState.createEntity('LINK', 'IMMUTABLE', entityData);
  const entityKey = contentState.getLastCreatedEntityKey();
  let selection = editorState.getSelection();
  if (selection.isCollapsed()) {
    contentState = Modifier.insertText(contentState, selection, entityData.name + ' ', undefined, entityKey);
  } else {
    contentState = Modifier.replaceText(contentState, selection, entityData.name + ' ', undefined, entityKey);
  }
  // move cursor to the end of the inserted link
  let end;
  contentState.getFirstBlock().findEntityRanges(
    character => character.getEntity() === entityKey,
    (_, _end) => { end = _end; }
  );
  let newEditorState = EditorState.set(editorState, { currentContent: contentState });
  selection = selection.merge({ anchorOffset: end, focusOffset: end });
  newEditorState = EditorState.forceSelection(newEditorState, selection);
  handleEditorChange(newEditorState);
};

After implementing the three features, the editor provides a functional comment box with length feedback, @mention highlighting, and clickable links.

Draft.js editor example
Draft.js editor example
Length counter UI
Length counter UI
@mention highlight example
@mention highlight example
Link insertion example
Link insertion example
rich-text-editorDraft.jsMentionsCompositeDecoratorEditorState
Qingyun Technology Community
Written by

Qingyun Technology Community

Official account of the Qingyun Technology Community, focusing on tech innovation, supporting developers, and sharing knowledge. Born to Learn and Share!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.