🎁 100 free AI Toolkit licenses – apply by August 15.Learn more

Search and Replace

Available for free

An accessible search and replace panel for Tiptap editors. It provides live result highlighting, a one-based result counter, wrap-around navigation, match-case and whole-word options, regular expression search, and replace or replace-all actions.

The component owns the search state and editor commands, but deliberately does not own its position or open state. You can dock it in a corner, place it in a popover, or render it inline.

Requires the Find and Replace extension

Register @tiptap/extension-find-and-replace on the same editor. Without the extension, the panel still renders but its controls are disabled by default. Set hideWhenUnavailable to true if the panel should not render when the extension is missing.

Installation

Add the component via the Tiptap CLI:

npx @tiptap/cli@latest add search-and-replace

Setup

Register the Find and Replace extension in your editor. The UI component includes styles for .find-and-replace-result and .find-and-replace-result-current, so disable the extension's injected highlight CSS when you use the included SCSS.

import { FindAndReplace } from '@tiptap/extension-find-and-replace'
import { StarterKit } from '@tiptap/starter-kit'

const editor = useEditor({
  immediatelyRender: false,
  extensions: [
    StarterKit,
    FindAndReplace.configure({
      injectCSS: false,
    }),
  ],
})

Search input changes are sent directly to the extension. The extension's searchDebounceMs option controls when results update; the UI does not add a second debounce.

Components

<SearchAndReplace />

The complete search and replace panel. Keep the component mounted and control it with open when you want the built-in Mod+F shortcut to reopen a closed panel.

Usage

'use client'

import { useState } from 'react'
import { EditorContent, EditorContext, useEditor } from '@tiptap/react'
import { FindAndReplace } from '@tiptap/extension-find-and-replace'
import { StarterKit } from '@tiptap/starter-kit'

import { SearchAndReplace, SearchAndReplaceButton } from '@/components/tiptap-ui/search-and-replace'

export default function MyEditor() {
  const [isSearchOpen, setIsSearchOpen] = useState(false)
  const editor = useEditor({
    immediatelyRender: false,
    extensions: [
      StarterKit,
      FindAndReplace.configure({
        injectCSS: false,
      }),
    ],
    content: '<p>Search this document for Tiptap.</p>',
  })

  return (
    <EditorContext.Provider value={{ editor }}>
      <SearchAndReplaceButton
        tabIndex={0}
        aria-expanded={isSearchOpen}
        data-active-state={isSearchOpen ? 'on' : 'off'}
        onClick={() => setIsSearchOpen((current) => !current)}
      />

      <div style={{ position: 'relative' }}>
        <SearchAndReplace
          open={isSearchOpen}
          onOpen={() => setIsSearchOpen(true)}
          onClose={() => setIsSearchOpen(false)}
          style={{
            position: 'absolute',
            top: '0.5rem',
            right: '0.5rem',
            zIndex: 10,
          }}
        />

        <EditorContent editor={editor} />
      </div>
    </EditorContext.Provider>
  )
}

When an open SearchAndReplace panel changes to open={false}, the component stays mounted, the closed panel is no longer displayed, result highlights are removed, and the entered search and replacement values are kept. Opening it again reapplies the stored search. Unmounting the component clears the search and pending work entirely.

The panel does not close itself when the user clicks outside it. If your layout needs outside-click dismissal, manage that behavior in the component that owns open.

Navigating results moves the highlight only. The editor selection stays where the user left it, so toolbar state, floating menus, and popovers such as the link popover do not react to the current match.

Props

Two separate states control the panel: open decides whether a mounted panel is shown or closed, and hideWhenUnavailable decides whether the component renders at all.

The component also forwards HTMLAttributes<HTMLDivElement>—except children—to the root panel.

NameTypeDefaultDescription
editorEditor | nullEditor from EditorContextThe Tiptap editor instance.
hideWhenUnavailablebooleanfalseRenders nothing when the Find and Replace extension is unavailable. Otherwise the panel still renders, with disabled controls.
scrollIntoViewOptionsScrollIntoViewOptions{ block: 'nearest', inline: 'nearest' }Options used when the current result is scrolled into view. Smooth scrolling becomes immediate when the user prefers reduced motion.
openbooleantrueWhether the mounted panel is open. Closing preserves entered values but suspends result highlighting.
onClose() => voidundefinedCalled by the close button or Escape. The callback must update the controlled open state.
onOpen() => voidundefinedCalled when Mod+F is pressed while the panel is closed. Provide it when the shortcut should reopen the panel.
enableShortcutbooleantrueBinds Mod+F for the whole page while the component is mounted and the extension is available, including while the panel is closed.
autoFocusSearchbooleantrueFocuses and selects the search input when the panel opens and the editor is ready.
classNamestringundefinedAdds a class to the root panel without removing tiptap-search-replace.

<SearchAndReplaceContent />

An exact alias of SearchAndReplace. Use this name when the panel is embedded as inline content, such as inside a collapsed mobile toolbar. It accepts the same SearchAndReplaceProps.

<SearchAndReplaceContent
  editor={editor}
  open={isSearchOpen}
  onOpen={() => setIsSearchOpen(true)}
  onClose={() => setIsSearchOpen(false)}
/>

<SearchAndReplaceButton />

A presentational trigger built on the shared Button primitive. It renders the search icon, tooltip, and Mod+F shortcut hint, but it does not manage panel state or run editor commands.

<SearchAndReplaceButton
  tabIndex={0}
  aria-expanded={isSearchOpen}
  data-active-state={isSearchOpen ? 'on' : 'off'}
  onClick={() => setIsSearchOpen((current) => !current)}
/>

The button accepts the shared ButtonProps. Its default tabIndex is -1 because toolbar primitives manage focus with roving tab stops. Set tabIndex={0} when you render it as a standalone button outside a managed toolbar.

Regular expression mode

Turn on Use regular expression to treat the search term as a regular expression source. The extension compiles these patterns with an RE2-compatible engine, so lookarounds and backreferences are not supported. The match-case and whole-word options stay available while regex mode is active, and the panel adds buttons that fill the inputs with example patterns.

Invalid patterns, and patterns the engine rejects as unsafe, return zero results without throwing. Regular expressions affect matching only: replacement text is inserted literally, so capture references such as $1 are not expanded.

For extension-level options and matching rules, including how each option behaves in regex mode, see the Find and Replace extension documentation.

Hooks

useSearchAndReplace()

A headless hook exposing the same reactive editor state and actions used by the prebuilt panel.

Usage

import { useSearchAndReplace } from '@/components/tiptap-ui/search-and-replace'

function CustomSearchPanel({ editor }) {
  const {
    isVisible,
    canSearch,
    searchTerm,
    replaceTerm,
    resultCountLabel,
    caseSensitive,
    canNavigate,
    canReplace,
    setSearchTerm,
    setReplaceTerm,
    toggleCaseSensitive,
    goToNext,
    replaceCurrent,
  } = useSearchAndReplace({
    editor,
    hideWhenUnavailable: true,
  })

  if (!isVisible) return null

  return (
    <div role="search">
      <input
        aria-label="Search"
        value={searchTerm}
        disabled={!canSearch}
        onChange={(event) => setSearchTerm(event.target.value)}
      />
      <input
        aria-label="Replace"
        value={replaceTerm}
        disabled={!canSearch}
        onChange={(event) => setReplaceTerm(event.target.value)}
      />
      <span>{resultCountLabel}</span>
      <button
        type="button"
        aria-pressed={caseSensitive}
        disabled={!canSearch}
        onClick={toggleCaseSensitive}
      >
        Match case
      </button>
      <button type="button" disabled={!canNavigate} onClick={goToNext}>
        Next
      </button>
      <button type="button" disabled={!canReplace} onClick={replaceCurrent}>
        Replace
      </button>
    </div>
  )
}

Configuration

NameTypeDefaultDescription
editorEditor | nullEditor from EditorContextThe Tiptap editor instance.
hideWhenUnavailablebooleanfalseWhether the hook reports isVisible: false when the Find and Replace extension is unavailable.
scrollIntoViewOptionsScrollIntoViewOptionsDEFAULT_SCROLL_INTO_VIEW_OPTIONSOptions used to reveal the current result without moving focus.

Return values

NameTypeDescription
editorEditor | nullThe resolved editor instance.
isVisiblebooleanWhether the search UI should render.
canSearchbooleanWhether the editor has the Find and Replace extension.
searchTermstringThe immediate search input value.
replaceTermstringThe immediate replacement input value.
appliedSearchTermstringThe search term currently applied by the extension after its debounce.
totalnumberNumber of current results.
currentIndexnumber | nullZero-based current-result index, or null when no result is selected.
resultCountLabelstringOne-based label such as 1 / 8, or 0 / 0 without results.
caseSensitivebooleanCurrent match-case state from extension storage.
wholeWordbooleanCurrent whole-word state from extension storage.
useRegexbooleanCurrent regular-expression state from extension storage.
canNavigatebooleanWhether at least one result can be selected.
canReplacebooleanWhether the editor is editable, a current result exists, and replacement text is non-empty.
canReplaceAllbooleanWhether the editor is editable, results exist, and replacement text is non-empty.
setSearchTerm(value: string) => voidUpdates the input immediately and forwards non-empty values to the extension. Empty text clears results at once.
setReplaceTerm(value: string) => voidUpdates the replacement value in the hook and extension.
toggleCaseSensitive() => voidToggles case-sensitive matching.
toggleWholeWord() => voidToggles whole-word matching.
toggleUseRegex() => voidToggles regular-expression matching.
goToNext() => booleanSelects the next result and wraps at the end.
goToPrevious() => booleanSelects the previous result and wraps at the beginning.
replaceCurrent() => booleanReplaces the current result and advances when possible.
replaceAll() => booleanReplaces every result in the current result set.
applySearch() => voidReapplies the stored search and replacement values, for example when reopening a mounted panel.
suspendSearch() => voidClears highlights and pending work while preserving the hook's input values.
clearSearch() => voidClears input values, pending work, and highlights.
labelstringAccessible label: Search and replace.
shortcutKeysstringMain shortcut key: mod+f.
IconReact.ComponentTypeSearch icon component.

The exported UseSearchAndReplaceReturn type is the inferred return type of this hook.

Utilities

Availability and storage helpers

import {
  getFindAndReplaceStorage,
  isFindAndReplaceAvailable,
  shouldShowSearchAndReplace,
} from '@/components/tiptap-ui/search-and-replace'

const available = isFindAndReplaceAvailable(editor)
const storage = getFindAndReplaceStorage(editor)
const visible = shouldShowSearchAndReplace({
  editor,
  hideWhenUnavailable: true,
})
  • isFindAndReplaceAvailable(editor) checks whether the findAndReplace extension is registered.
  • getFindAndReplaceStorage(editor) returns its reactive storage object, or null.
  • shouldShowSearchAndReplace({ editor, hideWhenUnavailable }) applies the component's visibility rule.

Result formatting and scrolling

import {
  DEFAULT_SCROLL_INTO_VIEW_OPTIONS,
  formatResultCount,
  scrollCurrentResultIntoView,
} from '@/components/tiptap-ui/search-and-replace'

formatResultCount(null, 0) // "0 / 0"
formatResultCount(0, 8) // "1 / 8"

scrollCurrentResultIntoView(editor, {
  block: 'center',
  inline: 'nearest',
})

scrollCurrentResultIntoView() resolves the current match from its document position and scrolls it without moving focus. It does nothing while focus is inside the editor and honors prefers-reduced-motion.

Exported constants

ConstantValuePurpose
SEARCH_AND_REPLACE_SHORTCUT_KEYmod+fOpen or refocus search.
NEXT_RESULT_SHORTCUT_KEYmod+shift+fSelect the next result from inside the panel.
PREVIOUS_RESULT_SHORTCUT_KEYmod+shift+dSelect the previous result from inside the panel.
SEARCH_RESULT_CLASSfind-and-replace-resultClass rendered by the extension for every match.
SEARCH_RESULT_CURRENT_CLASSfind-and-replace-result-currentClass rendered by the extension for the current match.
DEFAULT_SCROLL_INTO_VIEW_OPTIONS{ block: 'nearest', inline: 'nearest' }Default current-result scroll alignment.

The module also exports SearchAndReplaceProps, SearchAndReplaceContentProps, UseSearchAndReplaceConfig, UseSearchAndReplaceReturn, and SearchAndReplaceEditorState.

Keyboard Shortcuts

KeysBehavior
Mod+FFocuses and selects the search input while open. While closed, calls onOpen when provided; otherwise native browser search remains available.
ArrowDownSelects the next result while search is open and results exist.
ArrowUpSelects the previous result while search is open and results exist.
Enter in SearchSelects the next result.
Enter in ReplaceReplaces the current result when replacement is enabled.
Mod+Shift+FSelects the next result while focus is inside the panel.
Mod+Shift+DSelects the previous result while focus is inside the panel.
EscapeCalls onClose while focus is inside the panel.

Plain arrow-key navigation outside the panel does not take over editor content, form fields, or other text inputs.

While the component is mounted and the extension is available, whether the panel is open or closed, Mod+F is captured for the whole page—including other inputs and contenteditable areas—so the browser's find-in-page never competes with your own search. Set enableShortcut to false when a layout should leave Mod+F to the browser.

Styling and public selectors

The panel is unpositioned by default and has a width of 19rem with max-width: 100%. At viewports up to 480px, it can stretch to the available width. Add positioning through className, style, or an outer layout component.

These classes are public styling and query hooks:

  • tiptap-search-replace
  • tiptap-search-replace-header
  • tiptap-search-replace-count
  • tiptap-search-replace-nav-group
  • tiptap-search-replace-inputs
  • tiptap-search-replace-options
  • tiptap-search-replace-actions
  • button-group

The component also exposes:

  • role="dialog" and aria-label="Search and replace" on the root panel, with the result counter announced through aria-live="polite".
  • data-open="true|false" on the root panel.
  • data-search-replace-action="prev|next|close|replace|replace-all|regex-docs" on actions and the regex documentation link.
  • data-field="search-query|replace-query" on the inputs.
  • data-option="match-case|whole-words|use-regex" on matching controls.
  • data-regex-example="<pattern>" on each button that fills the fields with a regular expression example.

The result highlight selectors are .find-and-replace-result and .find-and-replace-result-current. If you override them, keep injectCSS: false on the extension so only one highlight stylesheet controls the result.

Requirements

Dependencies

  • @tiptap/react - Core Tiptap React integration
  • @tiptap/extension-find-and-replace - Search state, matching, navigation, and replacement commands
  • @tiptap/starter-kit - Baseline editor extensions used by the installed example
  • react-hotkeys-hook - Keyboard shortcut handling

Optional Dependencies

  • sass - SCSS styling support
  • sass-embedded - Sass compiler used by projects that select the embedded implementation

Referenced Components

  • use-tiptap-editor and use-composed-ref hooks
  • tiptap-utils
  • button, button-group, card, input-group, separator, and switch primitives
  • search-icon, arrow-right-icon, external-link-icon, chevron-up-icon, chevron-down-icon, close-icon, case-sensitive-icon, and whole-word-icon
  • styles shared style foundation

The button primitive additionally installs the check-icon used by its check variant.

For a framework-neutral command and storage example, see the custom Find and Replace UI guide.