Place annotations at the visible center of the viewport
This guide explains how to create an annotation at the center of whatever part of a PDF page you’re currently viewing, i.e. the visible area after zooming or panning and not just the page’s geometric center.
This is common when, for example, you want to mark something exactly in the region you’re focused on, regardless of scroll or zoom state.
How it works
The approach involves three main steps:
- Find the visible area — The page and viewport are both represented by document object model (DOM) elements. Calculating the intersection lets you know exactly which part of the page is onscreen.
- Map to PDF coordinates — The viewer works in both screen pixels (client coordinates) and PDF page coordinates (points). To place an annotation, convert the visible rectangle from pixel-based coordinates to PDF page coordinates.
- Calculate the center — Once you have the visible rectangle on the page (in PDF points), compute the midpoint, which is where you’ll anchor your annotation.
Implementation steps
To place an annotation at the visible center:
- Identify both the page element for the current page and the viewport container in the DOM.
- Use
getBoundingClientRectto get their positions in pixels. - Calculate their intersection to find the actual visible chunk of the page.
- Convert the visible rectangle to PDF coordinates using
transformContentClientToPageSpace. - Use these coordinates to place your annotation at the visual center.
Code example
The function below demonstrates how to implement this logic:
async function placeAnnotationAtVisibleCenter(instance) { const pageIndex = instance.viewState.currentPageIndex;
// Get viewport and page bounding rectangles. const viewportRect = instance.contentDocument .querySelector(".PSPDFKit-Viewport") .getBoundingClientRect(); const pageRect = instance.contentDocument .querySelector(`[data-page-index="${pageIndex}"]`) .getBoundingClientRect();
// Adjust for toolbar overlap if present. const toolbar = instance.contentDocument.querySelector(".PSPDFKit-Toolbar"); const toolbarHeight = toolbar ? toolbar.getBoundingClientRect().height : 0;
// Calculate visible intersection. const visibleLeft = Math.max(viewportRect.left, pageRect.left); const visibleTop = Math.max(viewportRect.top + toolbarHeight, pageRect.top); const visibleRight = Math.min(viewportRect.right, pageRect.right); const visibleBottom = Math.min(viewportRect.bottom, pageRect.bottom);
// Check for zero-area intersection (page not visible). if (visibleLeft >= visibleRight || visibleTop >= visibleBottom) { throw new Error( "Page is not visible in viewport - cannot place annotation", ); }
// Calculate center of visible area. const centerX = (visibleLeft + visibleRight) / 2; const centerY = (visibleTop + visibleBottom) / 2;
// Transform client coordinates to PDF page coordinates (handles zoom/pan). const pagePoint = instance.transformContentClientToPageSpace( new NutrientViewer.Geometry.Point({ x: centerX, y: centerY }), pageIndex, );
// Create annotation at the visible center. return instance.create( new NutrientViewer.Annotations.TextAnnotation({ pageIndex, boundingBox: new NutrientViewer.Geometry.Rect({ left: pagePoint.x - 50, top: pagePoint.y - 25, width: 100, height: 50, }), text: { format: "plain", value: "Annotation at visible center", }, }), );}Try this example in the Playground.
placeAnnotationAtVisibleCenterisn’t a built-in API method. This implementation usestransformContentClientToPageSpace, which automatically handles zoom and pan transformations, and includes checks for toolbar overlap and zero-area intersections, as outlined in the common pitfalls section below.
Why this approach is needed
Using this method enhances usability and ensures annotations are contextually relevant.
- Page center isn’t enough — Placing something at
page.width/2,page.height/2always puts it at the geometric middle, which can be frustrating if you’re zoomed far away. - Screen coordinates aren’t sufficient — Annotations require page-based coordinates to appear consistently across zooms and devices.
- ViewState doesn’t provide visible bounds — While ViewState tracks zoom and page indexes, it doesn’t include the visible rect/bounds; you need to compute it by calculating the intersection between the page and viewport elements.
Common pitfalls and edge cases
When implementing this functionality, be aware of the following:
- Toolbar overlap — If the toolbar is inside the viewer, subtract its height when calculating the visible area.
- Zero-area intersections — If the visible rect is empty (for example, page not in viewport), don’t create an annotation.
- DOM not loaded — The code must run after page/viewport elements are present in the DOM.
Further reading
For a deeper dive into mapping screen to page coordinates, refer to our coordinate space guide.