Navigate a document

Build page controls, search, paragraph highlights, and text highlights with the editor API.

Use the editor API to build page controls, an outline, a search panel, a glossary, or review findings. These methods scroll, select text, and highlight paragraphs or text without direct DOM access.

These methods belong to the browser Editor API, separate from the Office.js-shaped document model.

MethodPurposeMoves the selection
getTotalPages(), getCurrentPage(mode)Read the page count and the current page.No
scrollToPage(pageNumber)Show a page.No
getOutline(), scrollToBlock(blockId)List headings and show one of them.No
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
setHighlights(name, ranges, options)Mark text ranges, such as search results or 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

Try document navigation

This simulation uses sample text without loading the editor. To try the public API with a document, run the DOCX paragraph reference example.

Get the editor instance

In React and Vue, 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>
  );
}

If you use the core package directly, call these methods on the instance that createDocxEditor() returns. Prevent the default mousedown action on navigation buttons to keep the editor's caret during pointer interaction.

Go to a page

Page numbers start at one. getCurrentPage() returns the page at the caret. Pass 'viewport' to get the page at the center of the visible area instead:

const total = editor.getTotalPages();
const current = editor.getCurrentPage('viewport');
pageLabel.textContent = `Page ${current} of ${total}`;

if (!editor.scrollToPage(current + 1)) {
  // The page does not exist, or the editor has no scroll container.
}

scrollToPage() puts the top of the page near the top of the viewport, with 24 pixels of space. It returns false for a page number that is not a positive integer or does not exist. Without a measurable viewport, getCurrentPage('viewport') returns the caret page.

Go to a heading

getOutline() returns the headings in document order. Each entry has text, level, and blockId. Pass the blockId to scrollToBlock() to show the heading:

for (const heading of editor.getOutline()) {
  const item = document.createElement('button');
  item.textContent = heading.text;
  item.style.paddingInlineStart = `${(heading.level - 1) * 12}px`;
  item.addEventListener('mousedown', (event) => event.preventDefault());
  item.addEventListener('click', () => editor.scrollToBlock(heading.blockId));
  outline.append(item);
}

scrollToBlock() puts the heading near the top of the viewport. If a header, footer, or note is open for editing, the editor returns to the body first. Block IDs belong to the open document. After you load another document, read the outline again.

Scroll to a paragraph reference

To scroll to a paragraph by its w14:paraId, use editor.scrollToAnchor(anchor). You can store this ID with a review finding or receive it from a server.

Query paragraphs to get IDs from the open document. Store each paraId with its finding:

const paragraphs = editor.query({ type: 'paragraphs' });
const findings = paragraphs
  .filter((paragraph) => paragraph.paraId && paragraph.text.includes('indemnify'))
  .map((paragraph) => ({ paraId: paragraph.paraId!, label: paragraph.text }));

Pass a DocAnchor and check the boolean result:

import type { DocAnchor, Editor } from '@docx-editor.dev/core';

function revealReference(editor: Editor, anchor: DocAnchor): boolean {
  return editor.scrollToAnchor(anchor);
}

const revealed = revealReference(editor, { paraId: '1B4C77A2' });
if (!revealed) {
  // Tell the user that the reference is unavailable.
}

The method preserves selection, focus, editing scope, document content, and undo history. It also works in viewing mode. By default, it centers an offscreen target vertically. If the target is visible, it does not scroll.

To reveal a phrase within a paragraph, include search:

const revealed = editor.scrollToAnchor({
  paraId: '1B4C77A2',
  search: 'Acme GmbH',
  occurrence: 2,
});

Paragraph ID matching ignores case. Search text matching is exact and case-sensitive. Without occurrence, the phrase must appear exactly once. occurrence is a positive, one-based match number. The method reveals the start of the matched text, including when the paragraph spans pages.

Use paragraph IDs from the document open in the editor. Each call commits pending typing before it resolves the anchor against the open document. After you fetch document bytes, call editor.load(bytes) before scrolling. The editor completes any pending local document mount before resolving the anchor.

ResultMeaning
trueThe target is already visible, or the editor scrolls to it.
falseThe anchor is invalid, missing, ambiguous, unsupported, or has no layout position.
falseThe editor has no mounted document or measurable scroll container.

Check the return value to detect invalid references. The method does not select another paragraph or use a partial text match.

Body paragraphs, table cells, block content controls, headers, footers, footnotes, and endnotes support anchor scrolling. For repeated headers and footers, the method uses the first occurrence in the layout. Text boxes and document regions without layout positions return false.

Control scrolling

To control the target position, pass ScrollToAnchorOptions as the second argument. These fields match NavigateToChangeOptions in the document refresh API:

import type { ScrollToAnchorOptions } from '@docx-editor.dev/core';

const scroll: ScrollToAnchorOptions = {
  block: 'start',
  behavior: 'smooth',
  offsetPx: 48,
};
editor.scrollToAnchor({ paraId: '1B4C77A2' }, scroll);
OptionDefaultMeaning
block'centerIfNeeded'Use 'start', 'center', 'centerIfNeeded', or 'nearest'. 'centerIfNeeded' keeps the viewport still when the target is visible.
behavior'instant'Use 'smooth' for an animated scroll. Reduced-motion preferences make it instant.
offsetPx24Nonnegative edge padding for 'start' and 'nearest', in CSS pixels.

The method validates options before it resolves the anchor. An invalid block or behavior value throws TypeError. An invalid offsetPx value throws RangeError. The method returns when scrolling starts. It does not wait for a smooth scroll to finish.

Highlight a paragraph reference

Use editor.highlightAnchor(anchor, options) to highlight a referenced paragraph. To scroll to the paragraph before highlighting it, call both methods:

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 without moving text or scrolling. It accepts the same DocAnchor and matching rules as scrollToAnchor, including search. The search field validates the reference; it does not limit the highlight to the matching phrase. Each call replaces the previous highlight. A call for the same paragraph restarts the timeout without repeating the fade-in animation.

ResultMeaning
trueThe paragraph has a body layout position and receives the highlight, even when it is offscreen.
falseThe anchor is invalid, missing, ambiguous, unsupported, or has no layout position.
falseThe editor has no mounted document, or a document refresh is replacing it.

A false result leaves the current highlight unchanged. Body paragraphs, table cells, and block content controls support highlights. Headers, footers, footnotes, endnotes, and text boxes return false. A successful scrollToAnchor() call does not guarantee that highlightAnchor() supports the same target.

Customize the highlight

RefreshHighlightOptions extends AnchorHighlightOptions. Both use the same defaults, except for the color token. For details, see Customize highlights and motion. Each call starts from the defaults. To apply the same style across calls and APIs, reuse an options object:

import { createDocumentRefresh } from '@docx-editor.dev/core/editor';
import type { AnchorHighlightOptions } from '@docx-editor.dev/core';

const findingHighlight: AnchorHighlightOptions = {
  color: 'var(--finding-fill)',
  borderColor: 'var(--finding-border)',
  borderWidth: 2,
  borderStyle: 'dashed',
  opacity: 0.2,
  padding: 6,
  borderRadius: 8,
  className: 'finding-target',
  timeoutMs: 1500,
  animation: {
    durationMs: 180,
    exitDurationMs: 400,
    easing: 'cubic-bezier(0.23, 1, 0.32, 1)',
  },
};

editor.highlightAnchor({ paraId: '1B4C77A2', search: 'Acme GmbH' }, findingHighlight);
// The same object styles document refresh highlights.
createDocumentRefresh(editor).highlightChanges(findingHighlight);
OptionDefaultMeaning
colorvar(--doc-anchor-highlight-color)A CSS color or variable. The theme token defaults to light blue.
opacity0.14Fill opacity between 0 and 1. Borders and CSS class styles keep their own opacity.
padding4Extra space on each edge, in CSS pixels at 100% zoom.
borderRadius6Corner radius in CSS pixels at 100% zoom.
borderWidth0Border width in CSS pixels at 100% zoom.
borderColorSame as colorCSS border color, independent of fill opacity.
borderStyle'solid'Use 'solid', 'dashed', or 'dotted'.
classNameOmittedAdd CSS classes for shadows, outlines, or background patterns.
timeoutMs3000Milliseconds before dismissal starts. Use null to keep the highlight until you clear it.
animationtrueA 180-millisecond opacity fade. Use false for immediate changes.
animation.durationMs180Fade-in and default fade-out duration, from 0 to 10000 milliseconds.
animation.exitDurationMsSame as durationMsFade-out duration after the timeout or a clearAnchorHighlight() call.
animation.easing--doc-motion-ease-outCSS timing function for both fades. The method rejects CSS variables.

Set --doc-anchor-highlight-color in your application CSS to change the default color. This token is separate from --doc-refresh-highlight-color:

.docx-editor {
  --doc-anchor-highlight-color: #f59e0b;
}

The method validates options before it resolves the anchor. Invalid numeric settings throw RangeError, even when the paragraph does not exist. Invalid colors, border styles, or animation settings throw TypeError.

Control timing and dismissal

The timeout starts after each highlightAnchor() call. A timeout of 0 starts the dismissal on the next timer task. To keep the highlight until you dismiss it, use timeoutMs: null:

editor.highlightAnchor(anchor, { timeoutMs: null });

// For example, when the reader closes the finding:
editor.clearAnchorHighlight();
editor.clearAnchorHighlight({ animation: false });
editor.clearAnchorHighlight({ animation: { durationMs: 125 } });

clearAnchorHighlight() uses the exit duration and easing from the last highlightAnchor() call. It validates options even without a highlight. Loading or replacing the document removes the highlight immediately. Local edits keep the highlight on the same paragraph.

The highlight does not change selection, focus, document content, or undo history. The editor excludes it from saved files. Anchor highlights and refresh highlights use separate overlays, so dismissing one does not dismiss the other. Reduced-motion preferences limit fades to 125 milliseconds. Give each highlighted reference a text label to explain the finding without relying on color.

If the highlight must start after the target is visible, keep the default 'instant' scroll behavior. With behavior: 'smooth', the highlight can fade in before scrolling ends.

Find and select text

findMatches(query, options) returns up to 2,000 matches in document order. In editing mode, it searches supported body, header, footer, note, table, and text-box stories. Viewing mode restricts search to body stories, including tables and selectable body text boxes. Set matchCase or wholeWord to narrow the search:

const matches = editor.findMatches('Supplier', { matchCase: true, wholeWord: true });
let current = 0;

function showMatch(index: number) {
  const match = matches[index];
  if (!match) return;
  const result = editor.selectMatch(match);
  if (result.ok) {
    status.textContent = `Match ${index + 1} of ${matches.length}`;
  }
}

nextButton.addEventListener('click', () => {
  if (!matches.length) return;
  current = (current + 1) % matches.length;
  showMatch(current);
});

Each TextMatch includes the matched text. It can include contextBefore and contextAfter for a results list. The scope field names the header, footer, or note that contains a match. Finding text does not move the selection.

selectMatch() selects the match and shows it. If the match is in a header, footer, or note, the editor opens that region first. A match in a text box selects the text box drawing. The method does not move focus. It returns an ExecResult; check ok before you update your interface.

Matches refer to positions in the open document. After the document changes, search again.

Search and highlight

The Find tab highlights every match while it is open. It uses a second color for the active match. Enter and Shift+Enter move between matches. Set searchHighlight to highlight only the active match or hide highlights:

<DocxEditor.Navigation searchHighlight="active" />

Ctrl+F and Cmd+F open the browser's search by default, so a page that embeds the editor keeps its own search. To open Find with the shortcut instead, see Open Find with Ctrl+F.

Search from code

The Find tab, useDocumentSearch(), and createDocumentSearch(editor) share one search session per editor. A search you start from code also shows in the open Find tab.

This button highlights every occurrence of a term and selects the first one:

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>
    </>
  );
}

Without React or Vue:

import { createDocumentSearch } from '@docx-editor.dev/core/editor';

const search = createDocumentSearch(editor);
search.find('Supplier', { highlight: 'all', selectFirst: true });
nextButton.onclick = () => search.next();
search.subscribe(() => {
  const { activeIndex, matches } = search.getState();
  counter.textContent = `${activeIndex + 1} of ${matches.length}`;
});
MemberDescription
find(query, options)Searches now and returns the matches.
setQuery(query)Searches after the user stops typing.
next(), previous(), goTo(index)Selects a match and scrolls to it. Focus doesn't move.
clear()Clears the query, the results, and their highlights.
matches, activeIndex, activeMatchThe results and the current match.

find() options:

OptionDescription
matchCase, wholeWordNarrow the search.
selectFirstSelects the first match.
highlight'all', 'active', or 'none'. Highlights the results until clear().

useDocumentSearch() requests highlights while its component remains mounted. It highlights all results by default. If a component only reads results, pass useDocumentSearch({ highlight: 'none' }).

Highlight requests share the search session. With multiple requests, 'all' takes precedence over 'active'. A 'none' request does not hide another component's highlights. find() replaces its previous highlight request. clear() removes that request and empties the results.

setQuery() searches after 150 milliseconds without query changes. isPending reports the wait. Document edits refresh results after 150 milliseconds without edits, with a maximum wait of one second. Navigation refreshes pending document changes before selecting a match. Results contain up to 2,000 matches. When truncated is true, display the count as a minimum, such as 2000+.

Viewing mode permits body search navigation and programmatic selection. It keeps the caret hidden. Pointer actions and caret keys preserve the selection in viewing mode.

Open Find with Ctrl+F

The editor doesn't handle Ctrl+F or Cmd+F, so a page that embeds it keeps browser search. To open Find with the shortcut, control the pane:

import { useEffect, useState } from 'react';
import { DocxEditor } from '@docx-editor.dev/react';

function NavigationWithFindShortcut() {
  const [open, setOpen] = useState(false);
  const [tab, setTab] = useState<'headings' | 'find'>('headings');

  useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'f') {
        event.preventDefault();
        setOpen(true);
        setTab('find');
      }
    };
    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, []);

  return (
    <DocxEditor.Navigation open={open} onOpenChange={setOpen} tab={tab} onTabChange={setTab} />
  );
}

Highlight text ranges

Highlight your own ranges, such as glossary terms or review findings, as a named set. Each set is independent of the others and of search.

Pass a function to recompute ranges after document changes. It runs after 150 milliseconds without edits. Continuous edits trigger it within one second. Document loads run it immediately:

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;
}

Without React or Vue:

import { watchHighlights } from '@docx-editor.dev/core/editor';

const glossary = watchHighlights(editor, 'glossary', (editor) =>
  editor.findMatches(terms, { wholeWord: true }).flat()
);
// Later:
glossary.stop();

findMatches() with an array searches the document once and returns one list per term.

For a one-time set, call setHighlights() directly:

editor.setHighlights('findings', editor.findMatches('Supplier'));
editor.clearHighlights('findings'); // Or clearHighlights() for every set.
OptionDefaultDescription
colorvar(--doc-text-highlight-color)CSS color for every range.
activeIndex-1Index of the active range. -1 means none.
activeColorvar(--doc-text-highlight-active-color)CSS color for the active range.
classNameNoneCSS classes for every highlight.
priority0Integer from -1000 to 1000. Higher sets paint on top.

setHighlights() returns { applied, unavailable }. Invalid input throws TypeError or RangeError and preserves the existing set. For custom ranges, pass blockId, start, and length. start and length count UTF-16 units in paragraph text. For ranges computed outside the editor, add expectedText to prevent highlights over changed text.

Style highlights

Set colors as tokens, with a value for dark mode:

.docx-editor {
  --doc-text-highlight-color: #c7f0d8;
  --glossary-color: #dbeafe;
}

.docx-editor.dark {
  --doc-text-highlight-color: rgba(80, 200, 120, 0.35);
  --glossary-color: rgba(96, 165, 250, 0.35);
}

Each set renders in a [data-highlight-set] element. Highlights have the docx-text-highlight class, and the active one also has docx-text-highlight--active. To underline glossary terms:

.docx-editor [data-highlight-set='glossary'] .docx-text-highlight {
  background: transparent;
  border-bottom: 2px dotted #2563eb;
}

If a highlight carries meaning, also show that meaning as text.

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 { useCallback } from 'react';
import { useHighlightAt, useHighlights, type Editor, type TextMatch } from '@docx-editor.dev/react';

type GlossaryRange = TextMatch & { readonly definition: string };

function Glossary({ entries }: { entries: readonly { term: string; definition: string }[] }) {
  const findTerms = useCallback(
    (editor: Editor): GlossaryRange[] =>
      editor
        .findMatches(
          entries.map((entry) => entry.term),
          { wholeWord: true }
        )
        .flatMap((matches, index) =>
          matches.map((match) => ({ ...match, definition: entries[index]!.definition }))
        ),
    [entries]
  );
  useHighlights('glossary', findTerms);

  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>
  );
}

Without React or Vue, call editor.getHighlightsAt(clientX, clientY) from a pointermove handler.

Edits, saving, and limits

  • A highlight moves with its text when you edit before it. An edit inside a range hides it; undo shows it again.
  • Highlights don't change the document, selection, or undo history, and they aren't saved or shared with collaborators.
  • Loading, refreshing, or recovering the document removes all highlights.
  • Function sources recompute ranges after replacement. Fixed arrays apply again after refresh or recovery, but require replacement after ordinary loads.
  • Ranges can be in the body, tables, headers, footers, notes, and text boxes in the body.
  • Up to 32 sets can exist. A set highlights up to 10,000 ranges and counts the rest as unavailable.
  • The set name search is reserved for search.

Move the caret to a reference

To move the caret to a paragraph 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 command places the caret at the start of the paragraph or the matched text. It uses the same matching rules as scrollToAnchor.

Run the example

The DOCX paragraph reference example includes a React panel beside a live document. You can choose a reference, an effect, a highlight style, a duration, and scroll settings. The panel shows the corresponding API calls.

From the repository root, run:

bun install
bun run build:packages
bun run dev:anchors

Open http://localhost:5181.

See also