---
title: "Search and Replace"
description: "Add a complete search and replace panel with navigation, match options, regular expressions, and keyboard shortcuts to your Tiptap editor."
canonical_url: "https://tiptap.dev/docs/ui-components/components/search-and-replace"
---

# Search and Replace

Add a complete search and replace panel with navigation, match options, regular expressions, and keyboard shortcuts to your Tiptap editor.

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.

> **Interactive demo:** [search and replace](https://template.tiptap.dev/preview/tiptap-ui/search-and-replace)

> **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:

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

## Setup

Register the [Find and Replace extension](https://tiptap.dev/docs/editor/extensions/functionality/find-and-replace.md) 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.

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

```tsx
'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.

| Name                    | Type                    | Default                                   | Description                                                                                                                          |
| ----------------------- | ----------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `editor`                | `Editor \| null`        | Editor from `EditorContext`               | The Tiptap editor instance.                                                                                                          |
| `hideWhenUnavailable`   | `boolean`               | `false`                                   | Renders nothing when the Find and Replace extension is unavailable. Otherwise the panel still renders, with disabled controls.       |
| `scrollIntoViewOptions` | `ScrollIntoViewOptions` | `{ block: 'nearest', inline: 'nearest' }` | Options used when the current result is scrolled into view. Smooth scrolling becomes immediate when the user prefers reduced motion. |
| `open`                  | `boolean`               | `true`                                    | Whether the mounted panel is open. Closing preserves entered values but suspends result highlighting.                                |
| `onClose`               | `() => void`            | `undefined`                               | Called by the close button or `Escape`. The callback must update the controlled `open` state.                                        |
| `onOpen`                | `() => void`            | `undefined`                               | Called when `Mod+F` is pressed while the panel is closed. Provide it when the shortcut should reopen the panel.                      |
| `enableShortcut`        | `boolean`               | `true`                                    | Binds `Mod+F` for the whole page while the component is mounted and the extension is available, including while the panel is closed. |
| `autoFocusSearch`       | `boolean`               | `true`                                    | Focuses and selects the search input when the panel opens and the editor is ready.                                                   |
| `className`             | `string`                | `undefined`                               | Adds 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`.

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

```tsx
<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](https://tiptap.dev/docs/editor/extensions/functionality/find-and-replace.md).

## Hooks

### `useSearchAndReplace()`

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

#### Usage

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

| Name                    | Type                    | Default                            | Description                                                                                     |
| ----------------------- | ----------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------- |
| `editor`                | `Editor \| null`        | Editor from `EditorContext`        | The Tiptap editor instance.                                                                     |
| `hideWhenUnavailable`   | `boolean`               | `false`                            | Whether the hook reports `isVisible: false` when the Find and Replace extension is unavailable. |
| `scrollIntoViewOptions` | `ScrollIntoViewOptions` | `DEFAULT_SCROLL_INTO_VIEW_OPTIONS` | Options used to reveal the current result without moving focus.                                 |

#### Return values

| Name                  | Type                      | Description                                                                                                      |
| --------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `editor`              | `Editor \| null`          | The resolved editor instance.                                                                                    |
| `isVisible`           | `boolean`                 | Whether the search UI should render.                                                                             |
| `canSearch`           | `boolean`                 | Whether the editor has the Find and Replace extension.                                                           |
| `searchTerm`          | `string`                  | The immediate search input value.                                                                                |
| `replaceTerm`         | `string`                  | The immediate replacement input value.                                                                           |
| `appliedSearchTerm`   | `string`                  | The search term currently applied by the extension after its debounce.                                           |
| `total`               | `number`                  | Number of current results.                                                                                       |
| `currentIndex`        | `number \| null`          | Zero-based current-result index, or `null` when no result is selected.                                           |
| `resultCountLabel`    | `string`                  | One-based label such as `1 / 8`, or `0 / 0` without results.                                                     |
| `caseSensitive`       | `boolean`                 | Current match-case state from extension storage.                                                                 |
| `wholeWord`           | `boolean`                 | Current whole-word state from extension storage.                                                                 |
| `useRegex`            | `boolean`                 | Current regular-expression state from extension storage.                                                         |
| `canNavigate`         | `boolean`                 | Whether at least one result can be selected.                                                                     |
| `canReplace`          | `boolean`                 | Whether the editor is editable, a current result exists, and replacement text is non-empty.                      |
| `canReplaceAll`       | `boolean`                 | Whether the editor is editable, results exist, and replacement text is non-empty.                                |
| `setSearchTerm`       | `(value: string) => void` | Updates the input immediately and forwards non-empty values to the extension. Empty text clears results at once. |
| `setReplaceTerm`      | `(value: string) => void` | Updates the replacement value in the hook and extension.                                                         |
| `toggleCaseSensitive` | `() => void`              | Toggles case-sensitive matching.                                                                                 |
| `toggleWholeWord`     | `() => void`              | Toggles whole-word matching.                                                                                     |
| `toggleUseRegex`      | `() => void`              | Toggles regular-expression matching.                                                                             |
| `goToNext`            | `() => boolean`           | Selects the next result and wraps at the end.                                                                    |
| `goToPrevious`        | `() => boolean`           | Selects the previous result and wraps at the beginning.                                                          |
| `replaceCurrent`      | `() => boolean`           | Replaces the current result and advances when possible.                                                          |
| `replaceAll`          | `() => boolean`           | Replaces every result in the current result set.                                                                 |
| `applySearch`         | `() => void`              | Reapplies the stored search and replacement values, for example when reopening a mounted panel.                  |
| `suspendSearch`       | `() => void`              | Clears highlights and pending work while preserving the hook's input values.                                     |
| `clearSearch`         | `() => void`              | Clears input values, pending work, and highlights.                                                               |
| `label`               | `string`                  | Accessible label: `Search and replace`.                                                                          |
| `shortcutKeys`        | `string`                  | Main shortcut key: `mod+f`.                                                                                      |
| `Icon`                | `React.ComponentType`     | Search icon component.                                                                                           |

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

## Utilities

### Availability and storage helpers

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

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

| Constant                           | Value                                     | Purpose                                                |
| ---------------------------------- | ----------------------------------------- | ------------------------------------------------------ |
| `SEARCH_AND_REPLACE_SHORTCUT_KEY`  | `mod+f`                                   | Open or refocus search.                                |
| `NEXT_RESULT_SHORTCUT_KEY`         | `mod+shift+f`                             | Select the next result from inside the panel.          |
| `PREVIOUS_RESULT_SHORTCUT_KEY`     | `mod+shift+d`                             | Select the previous result from inside the panel.      |
| `SEARCH_RESULT_CLASS`              | `find-and-replace-result`                 | Class rendered by the extension for every match.       |
| `SEARCH_RESULT_CURRENT_CLASS`      | `find-and-replace-result-current`         | Class 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

| Keys               | Behavior                                                                                                                                        |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `Mod+F`            | Focuses and selects the search input while open. While closed, calls `onOpen` when provided; otherwise native browser search remains available. |
| `ArrowDown`        | Selects the next result while search is open and results exist.                                                                                 |
| `ArrowUp`          | Selects the previous result while search is open and results exist.                                                                             |
| `Enter` in Search  | Selects the next result.                                                                                                                        |
| `Enter` in Replace | Replaces the current result when replacement is enabled.                                                                                        |
| `Mod+Shift+F`      | Selects the next result while focus is inside the panel.                                                                                        |
| `Mod+Shift+D`      | Selects the previous result while focus is inside the panel.                                                                                    |
| `Escape`           | Calls `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](https://tiptap.dev/docs/guides/find-and-replace.md).
