# How to Implement Search and Replace Functionality in Tiptap Editor

> Learn to implement search and replace in Tiptap editor using the hunghg255/reactjs-tiptap-editor repository. This guide details its ProseMirror plugin, storage system, and React UI for a seamless experience.

- Repository: [Hung Hoang/reactjs-tiptap-editor](https://github.com/hunghg255/reactjs-tiptap-editor)
- Tags: how-to-guide
- Published: 2026-03-03

---

**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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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:

- [`src/extensions/SearchAndReplace/SearchAndReplace.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/extensions/SearchAndReplace/SearchAndReplace.ts) – Extension definition, storage interface, command implementations, and the ProseMirror plugin factory
- [`src/extensions/SearchAndReplace/components/RichTextSearchAndReplace.tsx`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/extensions/SearchAndReplace/components/RichTextSearchAndReplace.tsx) – React popover component that drives the commands and displays the match counter
- [`src/extensions/SearchAndReplace/index.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/extensions/SearchAndReplace/index.ts) – Public re-exports for cleaner imports

## Basic Implementation

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

```tsx
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:

```ts
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:

```css
.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`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/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.