This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/web/viewer/accessibility/keyboard-review-workflow.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Keyboard-accessible PDF review workflow | Nutrient Web SDK

Use this guide when you need one coherent implementation path for a keyboard-first review experience in Nutrient Web SDK.

Recommendation summary:

Workflow overview

A keyboard-accessible review flow should cover four areas together:

  1. Focus order and landmarks — Users must be able to reach host-app controls, the viewer, side panels, and document content in a predictable order.
  2. Keyboard actions — Save, export, annotation placement, and panel toggles should all work without a pointer device.
  3. Persistence semantics — Saving review data is different from exporting a final PDF.
  4. Status feedback — Announce save/export progress with a visible UI and an aria-live region.

If you only implement toolbar customization or export APIs in isolation, it’s common to end up with controls that are technically functional but incomplete for accessibility.

Use clear landmarks around the viewer and the surrounding review controls:

  • A header or top toolbar for global review actions such as Save review and Export PDF.
  • A main region for the viewer.
  • An optional aside for comments, instructions, or review metadata.
  • A dedicated status element with aria-live="polite".

A typical focus order looks as follows:

  1. Skip link (if your app uses one).
  2. Review action buttons in the host app.
  3. Viewer toolbar controls.
  4. Document content and annotations.
  5. Sidebar or comment thread.
  6. Secondary actions such as export/download.

When annotations need a custom tab sequence in standalone (Web SDK only) deployments, configure it with instance.setPageTabOrder(). For a position-based example, refer to the automatic annotation field tab ordering guide.

Keyboard shortcut strategy

Use shortcuts to accelerate review, but don’t rely on them as the only way to complete a task.

Recommended approach:

  • Keep every action available through a reachable button or menu item.
  • Avoid overriding common browser and assistive technology shortcuts.
  • Prefer app-scoped combinations such as Alt + Shift + S for Save review.
  • Offer a fallback path through visible controls when shortcuts conflict with screen readers, browser extensions, or OS-level commands.
  • Announce completion in the UI after each action.

Minimal runnable example

The example below creates a keyboard-first review shell around the viewer. It provides:

  • Native HTML buttons for host-app review actions.
  • A polite live region for save/export status.
  • Keyboard shortcuts for save and export.
  • Explicit separation between saving the review layer and exporting a final PDF.

HTML

Below is a minimal HTML structure for a review workflow. The viewer is contained within a main landmark, and the review actions are in a header landmark.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Keyboard-accessible review workflow</title>
<style>
body {
margin: 0;
font-family: sans-serif;
}
.app-shell {
display: grid;
grid-template-rows: auto 1fr;
height: 100vh;
}
.review-toolbar {
display: flex;
gap: 12px;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #d9d9d9;
}
.review-toolbar button {
min-height: 40px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
#viewer {
height: calc(100vh - 65px);
}
</style>
</head>
<body>
<a href="#viewer" class="sr-only">Skip to document viewer</a>
<div class="app-shell">
<header class="review-toolbar" aria-label="Review actions">
<button id="save-review">Save review</button>
<button id="export-final">Export PDF</button>
<span id="status-text" aria-hidden="true">Ready</span>
<div id="review-status" aria-live="polite" aria-atomic="true" class="sr-only"></div>
</header>
<main>
<div id="viewer"></div>
</main>
</div>
<script src="/path/to/pspdfkit.js"></script>
<script src="./index.js"></script>
</body>
</html>

JavaScript

Below is the JavaScript to load the viewer, restore previously saved review data from /api/reviews/current, handle review actions, and manage keyboard shortcuts.

let instance;
let lastSavedReview = null;
function announceStatus(message) {
document.getElementById("status-text").textContent = message;
document.getElementById("review-status").textContent = message;
}
async function fetchSavedReview() {
const response = await fetch("/api/reviews/current");
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error("Failed to load saved review data.");
}
return response.json();
}
async function saveReviewLayer() {
announceStatus("Saving review comments and annotations.");
const { pdfId, ...instantJSON } = await instance.exportInstantJSON();
// Replace this with your own persistence call.
const response = await fetch("/api/reviews/current", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(instantJSON)
});
if (!response.ok) {
announceStatus("Saving review failed.");
throw new Error("Failed to save review data.");
}
lastSavedReview = instantJSON;
announceStatus("Review saved.");
}
async function exportFinalPdf() {
announceStatus("Exporting PDF.");
const pdf = await instance.exportPDF();
const blob = new Blob([pdf], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "reviewed-document.pdf";
link.click();
URL.revokeObjectURL(url);
announceStatus("PDF export completed.");
}
function isEditableTarget(target) {
return (
target instanceof HTMLElement &&
(target.isContentEditable ||
["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName))
);
}
function registerKeyboardShortcuts() {
document.addEventListener("keydown", async (event) => {
if (isEditableTarget(event.target)) {
return;
}
if (event.altKey && event.shiftKey && event.key.toLowerCase() === "s") {
event.preventDefault();
await saveReviewLayer();
}
if (event.altKey && event.shiftKey && event.key.toLowerCase() === "e") {
event.preventDefault();
await exportFinalPdf();
}
});
}
async function loadViewer() {
lastSavedReview = await fetchSavedReview();
instance = await NutrientViewer.load({
container: "#viewer",
document: "/document.pdf",
baseUrl: `${window.location.origin}/assets/`,
toolbarItems: NutrientViewer.defaultToolbarItems,
instantJSON: lastSavedReview || undefined
});
document
.getElementById("save-review")
.addEventListener("click", saveReviewLayer);
document
.getElementById("export-final")
.addEventListener("click", exportFinalPdf);
registerKeyboardShortcuts();
announceStatus("Viewer ready.");
}
loadViewer().catch((error) => {
announceStatus("Viewer failed to load.");
console.error(error);
});

Persistence rules for accessible review flows

Below is a summary of the APIs to use for review-layer persistence and final document export. In an accessible workflow, it’s important to keep these actions separate and provide clear feedback on what each one does.

TaskUseWhy
Save comments, highlights, notes, and review progressinstance.exportInstantJSON()Saves the review layer without regenerating the PDF.
Restore a previously saved review layerConfiguration#instantJSON or instance.applyOperations()Rehydrates annotations when the document is loaded again.
Export the current document as PDFinstance.exportPDF()Produces a PDF file for download, delivery, or archive.
Export a finalized output with baked-in annotationsinstance.exportPDF() with { flatten: true }Makes annotations part of the final file when flattening is required.

Don’t use Export PDF as a substitute for Save review. In review and commenting workflows, users usually expect their annotation layer to remain editable and resumable. That means saving review data separately from final document export.

If you’re choosing an overall persistence model first, start with how to choose Instant JSON vs. XFDF vs. server-backed sync.

Viewer-toolbar and host-app integration

Many accessible review applications combine the built-in viewer UI with host-app controls.

Recommended pattern:

  • Put business-level actions such as Save review, Submit review, or Export final in your host app.
  • Keep annotation creation and document manipulation inside the viewer toolbar where possible.
  • Use native <button> elements for custom host controls.
  • Keep button text explicit; avoid icon-only controls unless they include an accessible name.
  • Return focus to the triggering control after modal dialogs or asynchronous flows complete.

If you’re building custom controls around the viewer, also review:

Keyboard-only review checklist

Use this checklist to validate the implementation:

  • Can the user reach all review actions with Tab?
  • Is the focus order predictable from host controls into the viewer?
  • Can the user create, select, and review annotations without a pointer device?
  • Does Save review preserve editable annotation data?
  • Does Export PDF create a document artifact instead of replacing review persistence?
  • Are status updates announced after save, export, and failure states?
  • Are shortcuts optional rather than required?
  • Is there a visible fallback for every shortcut-driven action?

Accessibility troubleshooting

Below are common issues that arise when implementing keyboard-accessible review workflows, along with tips for diagnosing and fixing them.

Focus trap between host app and viewer

If focus appears stuck:

  • Check for custom dialogs or side panels that don’t return focus on close.
  • Verify that hidden controls aren’t still focusable.
  • Make sure your host-app toolbar uses native interactive elements.

Toolbar focus handoff issues

If keyboard users lose context when moving from your app toolbar into the viewer:

  • Add a skip link or clearly ordered landmarks.
  • Keep the viewer container after the host toolbar in DOM order.
  • Avoid manually forcing focus unless a workflow step requires it.

Shortcut conflicts

If shortcuts don’t fire reliably:

  • Test with screen readers enabled.
  • Avoid single-key shortcuts.
  • Provide clickable alternatives for every shortcut.
  • Document the fallback path in your UI help or onboarding.