Keyboard-accessible review workflow
Use this guide when you need one coherent implementation path for a keyboard-first review experience in Nutrient Web SDK.
Recommendation summary:
- Review-layer save/restore — Use
instance.exportInstantJSON()to save annotation review data, and restore it withConfiguration#instantJSONorinstance.applyOperations(). - Final document export — Use
instance.exportPDF()only when you need a PDF file output for download, sharing, or archival. - Collaborative review — Use Document Engine with Instant synchronization when multiple users need to review the same document across sessions and devices.
Workflow overview
A keyboard-accessible review flow should cover four areas together:
- Focus order and landmarks — Users must be able to reach host-app controls, the viewer, side panels, and document content in a predictable order.
- Keyboard actions — Save, export, annotation placement, and panel toggles should all work without a pointer device.
- Persistence semantics — Saving review data is different from exporting a final PDF.
- Status feedback — Announce save/export progress with a visible UI and an
aria-liveregion.
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.
Recommended page structure
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:
- Skip link (if your app uses one).
- Review action buttons in the host app.
- Viewer toolbar controls.
- Document content and annotations.
- Sidebar or comment thread.
- 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.
| Task | Use | Why |
|---|---|---|
| Save comments, highlights, notes, and review progress | instance.exportInstantJSON() | Saves the review layer without regenerating the PDF. |
| Restore a previously saved review layer | Configuration#instantJSON or instance.applyOperations() | Rehydrates annotations when the document is loaded again. |
| Export the current document as PDF | instance.exportPDF() | Produces a PDF file for download, delivery, or archive. |
| Export a finalized output with baked-in annotations | instance.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.
Related guides
- Accessibility support for our JavaScript PDF viewer
- How to choose Instant JSON vs. XFDF vs. server-backed sync
- Importing and exporting annotations with Instant JSON
- Importing and exporting annotations with Document Engine
- Review persistence architecture