This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/web/viewer/accessibility/annotation-tab-order.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Annotation and form field tab order | Nutrient Web SDK

When a user presses Tab, Nutrient Web SDK moves focus through the annotations and form fields of a page in that page’s tab order. This guide explains where that order comes from and how to replace it with an order based on where each field sits on the page.

Tab order defined in the PDF

When a page’s /Tabs entry sets row order (/R), column order (/C), or widget order (/W), keyboard navigation in standalone mode follows it. Widget order requires 1.3.0 or later. In server-backed mode, keyboard navigation follows the document’s order starting with 1.19.0 and Document Engine 1.18.0.

To read a page’s current order, call instance.getPageTabOrder(). It returns the annotation IDs in tab order. In server-backed mode, it requires 1.19.0 or later and Document Engine 1.18.0 or later.

Sorting a page by field position

Use this approach when a document defines no tab order or one that doesn’t match its visual layout. It relies on instance.setPageTabOrder(), which works in standalone mode only.

The sorted order replaces the order defined in the PDF, and exporting the document keeps it. instance.exportPDF() writes the annotations of each sorted page in the new order and sets the page’s /Tabs entry to annotation order (/A).

A PDF page also draws its annotations in this order. Where two annotations overlap, the one that sorts later is on top in the exported PDF, even if the viewer shows the other one on top.

The following helpers sort the annotations of a page from top to bottom, and then from left to right:

// Compares annotations by position: top to bottom, then left to right.
const byPosition = (a, b) =>
a.boundingBox.top !== b.boundingBox.top
? a.boundingBox.top - b.boundingBox.top
: a.boundingBox.left - b.boundingBox.left;
const sortPageByPosition = (instance, pageIndex, changedAnnotations = []) =>
instance.setPageTabOrder(pageIndex, (tabOrderedAnnotations) => {
// On some versions before 1.19.0, newly created annotations aren't in their page's tab order yet.
const ids = new Set(tabOrderedAnnotations.map(({ id }) => id));
const annotations = [
...tabOrderedAnnotations,
...changedAnnotations.filter(({ id }) => !ids.has(id))
];
return annotations.sort(byPosition).map(({ id }) => id);
});
const sortAllPagesByPosition = (instance) =>
Promise.all(
Array.from({ length: instance.totalPageCount }, async (_, pageIndex) => {
// Before 1.19.0, `setPageTabOrder` doesn't load the page's annotations itself.
await instance.getAnnotations(pageIndex);
await sortPageByPosition(instance, pageIndex);
})
);
// Re-sorts a page after changes to its annotations are saved.
const keepTabOrderSorted = (instance) => {
// The latest version of each annotation created or updated since the last save.
const changedAnnotations = new Map();
const onChange = (annotations) => {
annotations.forEach((annotation) => changedAnnotations.set(annotation.id, annotation));
};
const onDelete = (annotations) => {
annotations.forEach((annotation) => changedAnnotations.delete(annotation.id));
};
const onSave = () => {
// Sort each page once, with all of its changed annotations.
const annotationsByPage = new Map();
changedAnnotations.forEach((annotation) => {
const pageAnnotations = annotationsByPage.get(annotation.pageIndex) || [];
annotationsByPage.set(annotation.pageIndex, [...pageAnnotations, annotation]);
});
changedAnnotations.clear();
annotationsByPage.forEach((pageAnnotations, pageIndex) => {
sortPageByPosition(instance, pageIndex, pageAnnotations).catch((error) =>
console.error(
`Failed to update the tab order of page index ${pageIndex} for annotations:`,
pageAnnotations.map(({ id }) => id),
error
)
);
});
};
instance.addEventListener("annotations.create", onChange);
instance.addEventListener("annotations.update", onChange);
instance.addEventListener("annotations.delete", onDelete);
instance.addEventListener("annotations.didSave", onSave);
};

The comparison uses exact top edges. On a row whose fields aren’t perfectly aligned, a field that sits even one point higher comes first.

The listener sorts after each save, because setPageTabOrder rejects an annotation that isn’t saved yet. With autoSaveMode set to DISABLED, the order only updates when instance.save() runs.

Sort every page once the document loads, and then keep the order sorted as changes are saved:

NutrientViewer.load({
container: "#nutrient",
document: "document.pdf"
})
.then(async (instance) => {
await sortAllPagesByPosition(instance);
keepTabOrderSorted(instance);
})
.catch((error) => {
console.error("Failed to load the document or set the tab order:", error);
});

Use 2024.3.2 or later. On 2024 versions, use PSPDFKit in place of NutrientViewer. Sorting loads the annotations of every page when the document opens, so it takes longer on documents with many pages.

setPageTabOrder is available from 2024.2.0, but the releases before 2024.3.2 have two gaps:

  • On 2024.2.0 and 2024.3.0 with autosave, two instance.create() calls in a row leave both annotations last in the tab order.
  • On 2024.3.1, pages that contain only form fields have no tab order after they load. Sorting them rejects with a does not have a tab order error, so keepTabOrderSorted never runs.