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 stringreplaceTerm– Replacement stringresults– Array ofRangeobjects marking match positionsresultIndex– Index of the currently active matchcaseSensitive– Boolean flag for case sensitivitylastSearchTerm,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:
src/extensions/SearchAndReplace/SearchAndReplace.ts– Extension definition, storage interface, command implementations, and the ProseMirror plugin factorysrc/extensions/SearchAndReplace/components/RichTextSearchAndReplace.tsx– React popover component that drives the commands and displays the match countersrc/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.
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)– Updatesstorage.searchTermand triggers a view updatesetReplaceTerm(term: string)– Updatesstorage.replaceTermsetCaseSensitive(flag: boolean)– Toggles case sensitivityresetIndex()– ResetsresultIndexto0nextSearchResult()– IncrementsresultIndex(wraps to start after last match)previousSearchResult()– DecrementsresultIndex(wraps to end before first match)replace()– Replaces the text in the currentresults[resultIndex]range withreplaceTerm, then removes that range from the results arrayreplaceAll()– Iterates over all ranges inresults, insertsreplaceTermat 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:
- A
DecorationSetcontainingDecoration.inlineobjects that wrap matched text with thesearch-resultclass (andsearch-result-currentfor the active index) - A
resultsarray 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 highlightsdisableRegex– Boolean; whentrue, treats the search term as a literal string rather than a regular expression
Summary
- The SearchAndReplace extension in
src/extensions/SearchAndReplace/SearchAndReplace.tscombines storage, commands, and a ProseMirror plugin into a single Tiptap extension - Storage at
editor.storage.searchAndReplaceholds the search term, results array, and current index, acting as the single source of truth - Commands like
setSearchTerm,replace, andreplaceAllmutate 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
processSearchesto generate highlight ranges - The UI component
RichTextSearchAndReplaceprovides 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →