Navigate a document
Build page controls, search, paragraph highlights, and text highlights with the editor API.
These methods belong to the browser Editor API, separate from the Office.js-shaped document model. They scroll, select, and highlight without direct DOM access.
Navigate from a custom control
Call useDocxEditor() inside DocxEditor.Root to get the editor instance. React returns an instance or null; Vue returns a shallow ref.
import { useDocxEditor } from '@docx-editor.dev/react';
function GoToEndButton() {
const editor = useDocxEditor();
return (
<button
disabled={!editor}
onMouseDown={(event) => event.preventDefault()}
onClick={() => editor?.scrollToPage(editor.getTotalPages())}
>
Go to the last page
</button>
);
}<script setup lang="ts">
import { useDocxEditor } from '@docx-editor.dev/vue';
const editor = useDocxEditor();
function goToEnd() {
editor.value?.scrollToPage(editor.value.getTotalPages());
}
</script>
<template>
<button :disabled="!editor" @mousedown.prevent @click="goToEnd">Go to the last page</button>
</template>With the core package, use the instance from createDocxEditor(). Prevent the default mousedown action to keep the caret.
| Method | Purpose | Moves the selection |
|---|---|---|
getTotalPages(), getCurrentPage(mode) | Read the page count and the current page. | No |
scrollToPage(pageNumber) | Show a page. | No |
getOutline() | List headings. | No |
scrollToBlock(blockId) | Show a heading or other body paragraph. | Only to leave a header, footer, note, or text frame. The body selection returns. |
scrollToAnchor(anchor, options) | Show a paragraph addressed by its w14:paraId. | No |
highlightAnchor(anchor, options) | Mark a paragraph with a temporary highlight. | No |
clearAnchorHighlight(options) | Dismiss the paragraph highlight. | No |
findMatches(query, options) | Find text in every document region. | No |
selectMatch(match) | Select a match and show it. | Yes |
exec({ type: 'replaceMatch', match, text }) | Replace a match where it is. | No |
setHighlights(name, ranges, options) | Mark text ranges, such as glossary terms. | No |
clearHighlights(name) | Remove one set of text highlights, or all of them. | No |
getHighlightsAt(clientX, clientY) | Read the text highlights under a pointer position. | No |
exec({ type: 'setSelection', anchor }) | Move the caret to a paragraph reference. | Yes |
Go to pages, headings, and paragraphs
Page numbers start at one. Pass 'viewport' to getCurrentPage() for the page at the center of the visible area instead of the caret page.
const current = editor.getCurrentPage('viewport');
pageLabel.textContent = `Page ${current} of ${editor.getTotalPages()}`;
editor.scrollToPage(current + 1); // false for a page that does not exist
for (const heading of editor.getOutline()) {
// Each entry has text, level, and blockId.
item.onclick = () => editor.scrollToBlock(heading.blockId, { block: 'center' });
}scrollToPage()puts the page top 24 CSS pixels below the viewport top, within scroll limits. It returnsfalsefor a page number that is not a positive integer, or when no measurable scroll container exists.- Without a measurable viewport,
getCurrentPage('viewport')returns the caret page. scrollToBlock()accepts{ block: 'start' | 'center' | 'centerIfNeeded' | 'nearest' }, with'start'as the default. It returnsfalsefor an unknown placement.
Block IDs belong to the open document. Read the outline again after you load another document.
Scroll to a paragraph reference
Store a paragraph's paraId with a finding, then pass it as a DocAnchor.
const findings = editor
.query({ type: 'paragraphs' })
.filter((paragraph) => paragraph.paraId && paragraph.text.includes('indemnify'))
.map((paragraph) => ({ paraId: paragraph.paraId!, label: paragraph.text }));
const revealed = editor.scrollToAnchor({
paraId: '1B4C77A2',
search: 'Acme GmbH', // optional phrase inside the paragraph
occurrence: 2,
});
if (!revealed) {
// Tell the user that the reference is unavailable.
}The method preserves selection, focus, document content, and undo history. It also works in viewing mode. It completes a pending document mount and commits pending typing before it resolves the anchor.
| Anchor field | Rule |
|---|---|
paraId | Required. Matching ignores case. |
search | Optional. Exact, case-sensitive match inside the paragraph. |
occurrence | Positive, one-based match number. Without it, search must match exactly once. |
scrollToAnchor() result | Meaning |
|---|---|
true | The target is visible, or the editor scrolls to it. |
false | The anchor is invalid, missing, ambiguous, unsupported, or has no layout position. The editor may also have no mounted document or scroll container. |
Pass ScrollToAnchorOptions as the second argument to control the position.
editor.scrollToAnchor({ paraId: '1B4C77A2' }, { block: 'start', behavior: 'smooth', offsetPx: 48 });| Option | Default | Description |
|---|---|---|
block | 'centerIfNeeded' | 'start', 'center', 'centerIfNeeded', or 'nearest'. 'centerIfNeeded' does not scroll a visible target. |
behavior | 'instant' | 'smooth' animates. Reduced-motion preferences make it instant. |
offsetPx | 24 | Nonnegative edge padding for 'start' and 'nearest', in CSS pixels. |
An invalid block or behavior throws TypeError. An invalid offsetPx throws RangeError. The method returns when scrolling starts.
Move the caret to a reference
Run setSelection with the same anchor, then focus the editor.
const result = editor.exec({
type: 'setSelection',
anchor: { paraId: '1B4C77A2', search: 'Acme GmbH' },
});
if (result.ok) editor.focus();The caret goes to the start of the paragraph or the matched text.
Search and replace text
findMatches() returns matches without moving the selection. selectMatch() selects one and returns an ExecResult.
const [first, second] = editor.findMatches('Supplier', { matchCase: true, wholeWord: true });
if (first && editor.selectMatch(first).ok) status.textContent = first.text;
if (second) {
const result = editor.exec({ type: 'replaceMatch', match: second, text: 'Vendor' });
if (!result.ok) console.warn(result.reason);
}selectMatch() opens a header, footer, or note when needed and does not move focus.
replaceMatch keeps the caret, selection, and scroll position. An empty text deletes the match. In suggesting mode, it records a tracked deletion and insertion with one undo step, and it refuses edits that cannot be tracked.
Search from code
The Find tab, useDocumentSearch(), and createDocumentSearch(editor) share one search session per editor.
import { useDocumentSearch } from '@docx-editor.dev/react';
function ShowOccurrences({ term }: { term: string }) {
const search = useDocumentSearch();
return (
<>
<button onClick={() => search.find(term, { selectFirst: true })}>Show “{term}”</button>
<button onClick={search.previous}>Previous</button>
<button onClick={search.next}>Next</button>
<span>
{search.activeIndex + 1} of {search.matches.length}
</span>
</>
);
}<script setup lang="ts">
import { useDocumentSearch } from '@docx-editor.dev/vue';
defineProps<{ term: string }>();
const search = useDocumentSearch();
</script>
<template>
<button @click="search.find(term, { selectFirst: true })">Show “{{ term }}”</button>
<button @click="search.previous">Previous</button>
<button @click="search.next">Next</button>
<span>{{ search.activeIndex.value + 1 }} of {{ search.matches.value.length }}</span>
</template>Without an adapter, call createDocumentSearch(editor) from @docx-editor.dev/core/editor, and read getState() in a subscribe() listener.
| Member | Description |
|---|---|
find(query, options) | Searches now and returns the matches. Options: matchCase, wholeWord, selectFirst, and highlight. Replaces the previous find() highlight request. |
setQuery(query) | Searches after 150 milliseconds without query changes. isPending reports the wait. |
next(), previous(), goTo(index) | Applies pending document changes to the results, then selects a match and scrolls to it. Focus does not move. |
clear() | Clears the query, results, and highlight request. |
matches, activeIndex, activeMatch | The results and the current match. Edits refresh the results after 150 milliseconds without edits, one second maximum. |
truncated | true when the result cap was reached. |
highlight is 'all', 'active', or 'none'. useDocumentSearch() highlights all results while mounted; pass { highlight: 'none' } to only read results. When requests differ, 'all' wins over 'active', and 'none' does not hide another component's highlights.
Highlight a paragraph reference
Call highlightAnchor() after scrollToAnchor() to mark the referenced paragraph.
import type { DocAnchor, Editor } from '@docx-editor.dev/core';
function showReference(editor: Editor, anchor: DocAnchor): boolean {
if (!editor.scrollToAnchor(anchor)) return false;
return editor.highlightAnchor(anchor, { timeoutMs: 1500 });
}The highlight marks the whole paragraph, even when search validates a phrase. Each call replaces the previous highlight. Local edits keep the highlight on its paragraph.
highlightAnchor() result | Meaning |
|---|---|
true | The paragraph has a body layout position, even offscreen. |
false | The paragraph has no body layout position. The current highlight stays unchanged. |
| Option | Default | Description |
|---|---|---|
color | var(--doc-anchor-highlight-color) | CSS color or variable. |
opacity | 0.14 | Fill opacity between 0 and 1. |
padding | 4 | Extra space on each edge, in CSS pixels at 100% zoom. |
borderRadius | 6 | Corner radius in CSS pixels at 100% zoom. |
borderWidth | 0 | Border width in CSS pixels at 100% zoom. |
borderColor | Same as color | CSS border color, independent of fill opacity. |
borderStyle | 'solid' | 'solid', 'dashed', or 'dotted'. |
className | Omitted | CSS classes for shadows, outlines, or patterns. |
timeoutMs | 3000 | Milliseconds before dismissal. null keeps the highlight until you clear it. |
animation | true | A 180-millisecond fade. false changes at once. An object sets durationMs (0 to 10000), exitDurationMs, and easing; CSS variables are rejected for easing. exitDurationMs defaults to durationMs, and easing defaults to --doc-motion-ease-out. |
RefreshHighlightOptions extends these options with a different default color token. For refresh-specific options, see Customize highlights and motion.
editor.highlightAnchor(anchor, { timeoutMs: null });
editor.clearAnchorHighlight({ animation: false });- Without overrides,
clearAnchorHighlight()uses the exit duration and easing of the last highlight. It validates the options you pass even when no highlight exists. - Anchor highlights and refresh highlights are independent. Clearing one does not clear the other.
Invalid numeric options throw RangeError, even when the paragraph does not exist. Invalid colors, border styles, or animation settings throw TypeError. To change the default color, set --doc-anchor-highlight-color on .docx-editor.
Highlight text ranges
Highlight your own ranges as a named set. Pass a function to recompute ranges after document changes.
import { useCallback } from 'react';
import { useHighlights, type Editor } from '@docx-editor.dev/react';
function Glossary({ terms }: { terms: readonly string[] }) {
const findTerms = useCallback(
(editor: Editor) => editor.findMatches(terms, { wholeWord: true }).flat(),
[terms]
);
useHighlights('glossary', findTerms, { color: 'var(--glossary-color)' });
return null;
}<script setup lang="ts">
import { useHighlights } from '@docx-editor.dev/vue';
const props = defineProps<{ terms: readonly string[] }>();
useHighlights('glossary', (editor) => editor.findMatches(props.terms, { wholeWord: true }).flat(), {
color: 'var(--glossary-color)',
});
</script>Without an adapter, call watchHighlights(editor, name, source) from @docx-editor.dev/core/editor, and call stop() on the result. For a one-time set, call editor.setHighlights('findings', matches). findMatches() with an array returns one list per term.
| Option | Default | Description |
|---|---|---|
color | var(--doc-text-highlight-color) | CSS color for every range. |
activeIndex | -1 | Index of the active range. -1 means none. |
activeColor | var(--doc-text-highlight-active-color) | CSS color for the active range. |
className | None | CSS classes for every highlight. |
priority | 0 | Integer from -1000 to 1000. Higher sets paint on top. |
setHighlights() returns { applied, unavailable }. Invalid input throws TypeError or RangeError and keeps the existing set.
- Custom ranges take
blockId,start, andlengthin UTF-16 units. - Add
expectedTextto ranges computed outside the editor.
Each set renders in a [data-highlight-set] element. Highlights have the docx-text-highlight class; the active one also has docx-text-highlight--active.
Show details on hover
Add your own fields to each range. useHighlightAt(name) returns the highlight under the pointer, with your range and a rect in client coordinates.
import { useHighlightAt, type TextMatch } from '@docx-editor.dev/react';
type GlossaryRange = TextMatch & { readonly definition: string };
function GlossaryTooltip() {
const hit = useHighlightAt<GlossaryRange>('glossary');
if (!hit) return null;
return (
<div role="tooltip" style={{ position: 'fixed', left: hit.rect.left, top: hit.rect.bottom }}>
{hit.range.definition}
</div>
);
}<script setup lang="ts">
import { useHighlightAt, type TextMatch } from '@docx-editor.dev/vue';
type GlossaryRange = TextMatch & { readonly definition: string };
const hit = useHighlightAt<GlossaryRange>('glossary');
</script>
<template>
<div
v-if="hit"
role="tooltip"
:style="{ position: 'fixed', left: `${hit.rect.left}px`, top: `${hit.rect.bottom}px` }"
>
{{ hit.range.definition }}
</div>
</template>Without an adapter, call editor.getHighlightsAt(clientX, clientY) from a pointermove handler.
Configure navigation shortcuts
Set shortcut and highlight props on the packaged navigation prop or a composed pane.
<DocxEditor document={file} navigation={{ findShortcut: false }} />
<DocxEditor.Navigation findShortcut={false} searchHighlight="active" /><template>
<DocxEditor :document="file" :navigation="{ findShortcut: false }" />
<DocxEditorNavigation :find-shortcut="false" search-highlight="active" />
</template>Open Find with Ctrl+F
| Prop | Description |
|---|---|
findShortcut | false leaves Ctrl+F (Cmd+F on macOS) to the browser. |
goToShortcut | false leaves Ctrl+G (Cmd+Option+G on macOS) to the browser. It opens the Bookmarks tab. |
searchHighlight | 'all', 'active', or 'none'. Sets which Find matches are highlighted. |
onTabChange, onOpenChange | Controlled-pane callbacks. A shortcut calls onTabChange('find') and onOpenChange(true). The pane gets focus only if your state opens it in the same update. |
The shortcuts work only while focus is in the same editor: its pages, pane, or chrome inside its container. Chrome outside a shared container counts only when the page has one editor. Elsewhere, the browser handles them.
Limits
- Paragraph IDs come from the open document. Call
editor.load(bytes)before you scroll to an ID from fetched bytes. scrollToAnchor()supports body, table cells, block content controls, headers, footers, footnotes, and endnotes. Repeated headers use their first occurrence. Text boxes returnfalse.highlightAnchor()supports body, table cells, and block content controls only.- Search caps results at 2,000. When
truncatedistrue, show the count as a minimum, such as2000+. - Viewing mode searches body stories only. Editing mode also searches headers, footers, notes, and text boxes.
- Matches go stale after document changes. Search again.
replaceMatchrequires the match's story to be the active one. A text-box match returnsunsupported.- Text highlights allow 32 sets and 10,000 ranges per set. Ranges past the limit count as unavailable and do not throw. The name
searchis reserved. - Text highlights support the body, tables, headers, footers, notes, and text boxes in the body.
- Highlights are not saved or shared, and they do not change selection or undo history. Give each one a text label.
- A text highlight moves with its text when you edit earlier text. An edit inside a highlighted range hides it; undo shows it again. Loading, refreshing, or recovering the document removes all highlights.
- Function sources recompute after edits (150 milliseconds idle, one second maximum) and loads. Fixed arrays return after refresh or recovery, not after an ordinary load.