How to Implement Search and Replace Functionality in Tiptap Editor

The hunghg255/reactjs-tiptap-editor repository provides a complete, production-ready extension that adds search and replace to Tiptap through a ProseMirror plugin for highlighting matches, a centralized storage system, and a React UI component.

To implement search and replace functionality in your Tiptap editor, you integrate the SearchAndReplace extension from the reactjs-tiptap-editor package. This extension registers a dedicated storage object, a set of chainable commands, and a ProseMirror plugin that automatically decorates text matches as the document changes.

Core Architecture

The search and replace system splits responsibilities across four distinct layers to keep the UI reactive and the document state synchronized.

Extension Layer

The SearchAndReplace class, defined in src/extensions/SearchAndReplace/SearchAndReplace.ts, declares the extension’s name, storage schema, and configuration options. It exposes the searchResultClass option (default: "search-result") and disableRegex boolean to control whether user input is treated as a literal string or a regular expression.

Storage System

State lives in the SearchAndReplaceStorage interface attached to editor.storage.searchAndReplace. The storage tracks:

  • searchTerm – Current query string
  • replaceTerm – Replacement string
  • results – Array of Range objects marking match positions
  • resultIndex – Index of the currently active match
  • caseSensitive – Boolean flag for case sensitivity
  • lastSearchTerm, lastCaseSensitive, lastResultIndex – Cache fields that let the plugin detect when it must recompute decorations

Commands mutate this storage directly, while the ProseMirror plugin reads from it to decide when to refresh highlights.

ProseMirror Plugin

Inside addProseMirrorPlugins(), the extension creates a plugin keyed by richtextCustomPlugin${this.name}. On every transaction, the plugin compares the current storage values against the cached "last" values. If the search term or case sensitivity changed, the plugin invokes processSearches() to walk the document nodes, execute String.prototype.matchAll against the compiled regex, and build a DecorationSet of inline highlights.

The active match (determined by resultIndex) receives an additional CSS class suffix (-current) so the UI can style the focused result differently.

UI Component

RichTextSearchAndReplace.tsx, located in src/extensions/SearchAndReplace/components/, renders a popover interface with two text inputs, navigation buttons, and replace actions. It reads the editor instance via useEditorInstance(), subscribes to storage changes through useEffect hooks, and forwards user interactions to the extension commands.

Source File Locations

Reference these files when customizing behavior or debugging:

Basic Implementation

Install the extension into your Tiptap editor configuration and render the UI component in your toolbar.

import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { SearchAndReplace } from '@/extensions/SearchAndReplace';
import { RichTextSearchAndReplace } from '@/extensions/SearchAndReplace/components/RichTextSearchAndReplace';

function App() {
  const editor = useEditor({
    extensions: [
      StarterKit,
      SearchAndReplace.configure({
        searchResultClass: 'my-highlight',
        disableRegex: false, // Allow regex patterns
      }),
    ],
    content: '<p>Search and replace demo content</p>',
  });

  if (!editor) return null;

  return (
    <div className="editor-container">
      <RichTextSearchAndReplace />
      <EditorContent editor={editor} />
    </div>
  );
}

The SearchAndReplace extension automatically registers its storage and plugin when added to the extensions array. The RichTextSearchAndReplace component connects to the same editor instance and renders the control interface without additional wiring.

Available Commands

The extension injects the following commands into the Tiptap command chain, accessible via editor.commands:

  • setSearchTerm(term: string) – Updates storage.searchTerm and triggers a view update
  • setReplaceTerm(term: string) – Updates storage.replaceTerm
  • setCaseSensitive(flag: boolean) – Toggles case sensitivity
  • resetIndex() – Resets resultIndex to 0
  • nextSearchResult() – Increments resultIndex (wraps to start after last match)
  • previousSearchResult() – Decrements resultIndex (wraps to end before first match)
  • replace() – Replaces the text in the current results[resultIndex] range with replaceTerm, then removes that range from the results array
  • replaceAll() – Iterates over all ranges in results, inserts replaceTerm at each position, and recalculates document offsets to prevent overlapping edits

All commands return false to indicate they handle state mutations manually rather than creating new Tiptap transactions.

How the ProseMirror Plugin Works

The plugin defined in addProseMirrorPlugins() executes the following logic on every state change:

new Plugin({
  key: new PluginKey(`richtextCustomPlugin${this.name}`),
  state: {
    init: () => DecorationSet.empty,
    apply({ doc, docChanged }, oldState) {
      const storage = editor.storage.searchAndReplace;
      
      // Skip recomputation if nothing changed
      if (!docChanged && 
          storage.lastSearchTerm === storage.searchTerm &&
          storage.lastCaseSensitive === storage.caseSensitive) {
        return oldState;
      }

      // Update cache values
      storage.lastSearchTerm = storage.searchTerm;
      storage.lastCaseSensitive = storage.caseSensitive;
      storage.lastResultIndex = storage.resultIndex;

      if (!storage.searchTerm) {
        storage.results = [];
        return DecorationSet.empty;
      }

      const regex = getRegex(storage.searchTerm, false, storage.caseSensitive);
      const { decorationsToReturn, results } = processSearches(
        doc,
        regex,
        'search-result',
        storage.resultIndex,
      );

      storage.results = results;
      return decorationsToReturn;
    },
  },
  props: {
    decorations(state) {
      return this.getState(state);
    },
  },
});

The processSearches helper function iterates over all text nodes in the document, runs matchAll against the compiled regular expression, and constructs two outputs:

  1. A DecorationSet containing Decoration.inline objects that wrap matched text with the search-result class (and search-result-current for the active index)
  2. A results array of { from, to } ranges used by the UI for navigation and replacement

Customizing Styles and Behavior

CSS Classes

Matches receive the class defined by searchResultClass (default: search-result). The currently focused match appends -current. Override these in your global stylesheet:

.search-result {
  background-color: rgba(255, 255, 0, 0.3);
  border-radius: 2px;
}

.search-result-current {
  background-color: rgba(255, 165, 0, 0.6);
  outline: 2px solid orange;
}

Extension Options

Pass these options to .configure() when initializing the extension:

  • searchResultClass – String defining the base CSS class for highlights
  • disableRegex – Boolean; when true, treats the search term as a literal string rather than a regular expression

Summary

  • The SearchAndReplace extension in src/extensions/SearchAndReplace/SearchAndReplace.ts combines storage, commands, and a ProseMirror plugin into a single Tiptap extension
  • Storage at editor.storage.searchAndReplace holds the search term, results array, and current index, acting as the single source of truth
  • Commands like setSearchTerm, replace, and replaceAll mutate storage and trigger view updates without creating new editor transactions
  • The ProseMirror plugin recomputes decorations automatically when the document changes or search parameters update, using processSearches to generate highlight ranges
  • The UI component RichTextSearchAndReplace provides a complete popover interface that wires user input to the underlying commands

Frequently Asked Questions

How do I add search and replace to an existing Tiptap editor?

Import the SearchAndReplace extension from src/extensions/SearchAndReplace and append it to your extensions array when calling useEditor(). Then render <RichTextSearchAndReplace /> anywhere in your React tree. The component automatically detects the editor instance and displays the search popover.

Can users search using regular expressions?

Yes. Set disableRegex: false in the extension configuration. When disabled, the extension treats the search term as a regular expression pattern. When true (the default), the term is escaped and treated as a literal string.

How do I change the highlight colors for search results?

Override the CSS classes .search-result and .search-result-current in your stylesheet. The base class is configurable via the searchResultClass option if you need to avoid style conflicts.

Does the extension handle large documents efficiently?

The extension walks the entire document on every relevant change to rebuild the result list. For documents with tens of thousands of nodes, consider debouncing the setSearchTerm command in your UI layer or implementing a web worker for the regex matching logic to avoid blocking the main thread.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →