Build custom search controls
Drive the Editor's visual search session from application-owned controls.
Use ui.search when your application owns the find interface but still needs SuperDoc to highlight, reveal, navigate, and replace matches in the open Editor.
This is an Editor search session. For programmatic content discovery and durable mutation targets, use Document API queries.
Add the controls
Create a query field, navigation controls, replacement actions, status output, and Editor container:
<form id="search-controls" role="search">
<input id="search-query" type="search" placeholder="Find in document" aria-label="Find in document" />
<label><input id="match-case" type="checkbox" /> Match case</label>
<button id="previous-match" type="button" disabled>Previous</button>
<button id="next-match" type="button" disabled>Next</button>
<output id="search-count" for="search-query">No matches</output>
<input id="replacement" type="text" placeholder="Replacement" aria-label="Replacement text" />
<button id="replace-match" type="button" disabled>Replace</button>
<button id="replace-all" type="button" disabled>Replace all</button>
</form>
<p id="search-status" role="status"></p>
<div id="editor" style="height: 70vh"></div>
<script type="module" src="/src/main.ts"></script>
Copy the tracked-changes fixture to your app's public directory as contract.docx, or use another DOCX.
Bind the search handle
Read the Editor's controller, superdoc.ui, after readiness, then observe ui.search as the source of match and availability state.
import { SuperDoc } from 'superdoc';
import type { WorkflowActionResult } from 'superdoc/ui';
import 'superdoc/style.css';
const query = document.querySelector<HTMLInputElement>('#search-query');
const matchCase = document.querySelector<HTMLInputElement>('#match-case');
const previous = document.querySelector<HTMLButtonElement>('#previous-match');
const next = document.querySelector<HTMLButtonElement>('#next-match');
const count = document.querySelector<HTMLOutputElement>('#search-count');
const replacement = document.querySelector<HTMLInputElement>('#replacement');
const replace = document.querySelector<HTMLButtonElement>('#replace-match');
const replaceAll = document.querySelector<HTMLButtonElement>('#replace-all');
const status = document.querySelector<HTMLParagraphElement>('#search-status');
if (!query || !matchCase || !previous || !next || !count || !replacement || !replace || !replaceAll || !status) {
throw new Error('The search UI is incomplete.');
}
let stopSearch: (() => void) | null = null;
let removeHandlers: (() => void) | null = null;
const superdoc = new SuperDoc({
selector: '#editor',
document: '/contract.docx',
onReady: ({ superdoc: readySuperDoc }) => {
const ui = readySuperDoc.ui;
const render = (search: ReturnType<typeof ui.search.getSnapshot>) => {
const hasMatches = search.total > 0;
previous.disabled = !hasMatches;
next.disabled = !hasMatches;
replace.disabled = !search.canReplace;
replaceAll.disabled = !search.canReplace;
count.textContent = hasMatches ? `${search.activeIndex + 1} of ${search.total}` : 'No matches';
status.textContent = search.reason ?? '';
};
const runSearch = () => {
if (!query.value) {
ui.search.clear();
return;
}
const opened = ui.search.open();
if (!opened.ok) {
status.textContent = opened.reason ?? 'Search is unavailable.';
return;
}
render(ui.search.search(query.value, { caseSensitive: matchCase.checked }));
};
const reportAction = (result: WorkflowActionResult) => {
if (!result.ok) status.textContent = result.reason ?? 'The search action did not run.';
};
const replaceCurrent = async () => {
reportAction(await ui.search.replace(replacement.value));
};
const replaceEveryMatch = async () => {
reportAction(await ui.search.replaceAll(replacement.value));
};
const goPrevious = () => reportAction(ui.search.previous());
const goNext = () => reportAction(ui.search.next());
render(ui.search.getSnapshot());
stopSearch = ui.search.observe(render);
query.addEventListener('input', runSearch);
matchCase.addEventListener('change', runSearch);
previous.addEventListener('click', goPrevious);
next.addEventListener('click', goNext);
replace.addEventListener('click', replaceCurrent);
replaceAll.addEventListener('click', replaceEveryMatch);
removeHandlers = () => {
query.removeEventListener('input', runSearch);
matchCase.removeEventListener('change', runSearch);
previous.removeEventListener('click', goPrevious);
next.removeEventListener('click', goNext);
replace.removeEventListener('click', replaceCurrent);
replaceAll.removeEventListener('click', replaceEveryMatch);
};
},
});
window.addEventListener('beforeunload', () => {
stopSearch?.();
removeHandlers?.();
superdoc.destroy();
});
The snapshot provides the state needed by a focused search interface:
totalandactiveIndexdescribe the current match.availableandreasonexplain whether the host can search.canReplacereflects document mode, host support, and whether the complete match set can be changed safely.query,caseSensitive, andregexdescribe the active session.
Observe after starting a search
search() returns the best-known snapshot immediately. A worker-backed search can settle later, so keep the interface subscribed with observe() instead of treating the first return value as final.
next() and previous() update the active match and reveal it in the document. Check their ok result before announcing navigation.
Await replacements
replace() and replaceAll() can settle asynchronously. Await them before saving the document or starting dependent work.
A failed result includes a stable reason such as an unavailable search host, a read-only document, or an operation the current session cannot perform. Keep replace controls synchronized with canReplace and still inspect the returned result.
Call clear() when the query becomes empty. Call close() when the application dismisses the search experience and should remove its highlights.
For the standard interface, use Find and replace in the built-in UI.