Back to blog
Developer GuideJanuary 30, 20258 min read

Add Visual Proof to your PDFs with Interactive Overlays.

This guide shows you how to build document viewers that don't just extract data, but show users exactly where that data came from. Transform user trust with coordinate-backed visual proof in just 15 lines of code.

Introduction

This guide shows you how to build document viewers that don't just extract data, but show users exactly where that data came from. Transform user trust with coordinate-backed visual proof in just 15 lines of code.

15-Line Setup

Copy our code and add overlays to any PDF viewer.

Automatic Scaling

No coordinate math required. Works with any container size.

Build Trust

Show users exactly where data came from with interactive highlights.

The Problem: Users Don't Trust 'Black Box' Extraction

You've built a document processing system that extracts data perfectly. But when you show users the results, they ask: "How do I know this is correct?" Without visual proof, users are forced to manually verify every field against the original document.

Traditional document viewers show the PDF and the extracted JSON separately. Users have to hunt through the document to find where "Total: $1,250.00" actually appears. This creates a trust gap that kills adoption and forces expensive manual review processes.

Prerequisites

Before you start, you'll need:

  • SDK Installation: npm i @ninjadoc-ai/sdk@^1.0.7 react-pdf
  • PDF Worker Setup: Configure PDF.js worker for client-side rendering (see public/pdf.worker.mjs)
  • API Key: Get your Ninjadoc API key from the dashboard
  • BFF Routes: Set up backend routes to proxy API calls (see below)
  • Processed Document: A document that has been processed through the Ninjadoc API

Security Note

Never expose your API key in browser-side code. The SDK uses BFF routes to keep your API key secure server-side.

Compatibility

SDK v1.0.7+ supports React 16.8+, TypeScript 4.5+, and modern browsers with ES2020 support. For older environments, consider using the direct API approach.

BFF Route Setup (Required)

Before using the SDK, set up these backend routes in your application:

BFF Route Configuration

// app/routes/api.ninjadoc.$.tsx
import { createRemixApiRoute } from '@ninjadoc-ai/sdk/frameworks/remix';

const { loader, action } = createRemixApiRoute({
  // API key automatically read from NINJADOC_API_KEY env var
});

export { loader, action };

// Add to app/routes.ts:
// route("/api/ninjadoc/*", "routes/api.ninjadoc.$.tsx")

This creates secure proxy routes that the SDK calls from your browser, keeping your API key safe on the server.

Quick Start: The 15-Line Interactive Overlay

Instead of building complex coordinate systems, our SDK handles all the math. You get automatic scaling, multi-page support, and interactive highlights that work with any PDF viewer. Just pass in your processed document and container size.

Minimal Interactive Viewer

// The simplest possible overlay implementation - just 15 lines!
import { createBrowserClient } from '@ninjadoc-ai/sdk';
import { HighlightOverlay } from '@ninjadoc-ai/sdk/react';
import { useState, useEffect } from 'react';

function DocumentViewer({ jobId, pdfFile }) {
  const [document, setDocument] = useState(null);
  const client = createBrowserClient({ baseUrl: window.location.origin });

  useEffect(() => {
    // Single method returns everything: regions + page dimensions + job status
    client.processDocument(jobId).then(setDocument);
  }, [jobId]);

  if (!document) return <div>Loading...</div>;

  return (
    <div style={{ position: 'relative' }}>
      {/* Your PDF viewer component goes here */}
      <PDFViewer file={pdfFile} width={800} height={600} />
      
      {/* Automatic coordinate scaling - no math required! */}
      <HighlightOverlay
        document={document}
        containerSize={{ width: 800, height: 600 }}
        onRegionClick={(region) => {
          alert(`Found: ${region.metadata.extractedText}`);
        }}
      />
    </div>
  );
}

Production Implementation: Multi-Page with Polling

This production-ready component handles the complete workflow: document processing with polling, multi-page navigation, error handling, and interactive region selection. The overlay automatically scales coordinates to match your PDF viewer dimensions.

Container Sizing Tips

  • • Use a container ref to measure actual rendered dimensions
  • • Pass those measurements to HighlightOverlay's containerSize prop
  • • Recompute on window resize for responsive accuracy
  • • Account for padding/margins in your container calculations

Production Document Viewer

// Production-ready implementation with polling, error handling, and multi-page support
import { createBrowserClient } from '@ninjadoc-ai/sdk';
import { HighlightOverlay } from '@ninjadoc-ai/sdk/react';
import { Document, Page } from 'react-pdf';
import { useState, useEffect } from 'react';

// Custom hook for document processing with polling (BFF-only, no secrets)
function useDocumentProcessing(jobId) {
  const [document, setDocument] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!jobId) return;

    const client = createBrowserClient({ baseUrl: window.location.origin });
    
    const pollForCompletion = async () => {
      try {
        let status;
        do {
          status = await client.getJobStatus(jobId);
          if (status.status === 'completed') {
            const processedDoc = await client.processDocument(jobId);
            setDocument(processedDoc);
            setLoading(false);
            return;
          } else if (status.status === 'failed') {
            throw new Error('Processing failed');
          }
          await new Promise(resolve => setTimeout(resolve, 2000));
        } while (['queued', 'processing'].includes(status.status));
      } catch (err) {
        setError(err.message);
        setLoading(false);
      }
    };

    pollForCompletion();
  }, [jobId]);

  return { document, loading, error };
}

// Production component with multi-page support
function ProductionDocumentViewer({ jobId, pdfFile, apiKey }) {
  const { document, loading, error } = useDocumentProcessing(jobId, apiKey);
  const [currentPage, setCurrentPage] = useState(1);
  const [numPages, setNumPages] = useState(null);
  const [pageSize, setPageSize] = useState({ width: 0, height: 0 });
  const [activeRegion, setActiveRegion] = useState(null);

  if (loading) return <div className="text-center p-8">Processing document...</div>;
  if (error) return <div className="text-red-600 p-8">Error: {error}</div>;
  if (!document) return null;

  // Filter regions for current page
  const currentPageRegions = document.regions.filter(region => region.page === currentPage);

  return (
    <div className="space-y-4">
      {/* Page Navigation */}
      {numPages > 1 && (
        <div className="flex items-center justify-center gap-4">
          <button 
            onClick={() => setCurrentPage(p => Math.max(p - 1, 1))}
            disabled={currentPage <= 1}
            className="px-4 py-2 bg-blue-600 text-white  disabled:bg-gray-400"
          >
            Previous
          </button>
          <span>Page {currentPage} of {numPages}</span>
          <button 
            onClick={() => setCurrentPage(p => Math.min(p + 1, numPages))}
            disabled={currentPage >= numPages}
            className="px-4 py-2 bg-blue-600 text-white  disabled:bg-gray-400"
          >
            Next
          </button>
        </div>
      )}

      {/* Document Viewer with Overlays */}
      <div className="relative border  overflow-hidden">
        <Document
          file={pdfFile}
          onLoadSuccess={({ numPages }) => setNumPages(numPages)}
        >
          <div className="relative">
            <Page
              pageNumber={currentPage}
              width={800}
              onRenderSuccess={(page) => setPageSize({ width: page.width, height: page.height })}
            />
            
            {/* The magic happens here - automatic coordinate scaling! */}
            <HighlightOverlay
              document={{
                ...document,
                regions: currentPageRegions // Only show current page regions
              }}
              containerSize={pageSize}
              onRegionClick={(region) => {
                setActiveRegion(region.id);
                // Build your verification UI here
                console.log('Clicked region:', region.label, region.metadata.extractedText);
              }}
              activeRegionId={activeRegion}
            />
          </div>
        </Document>
      </div>

      {/* Region Details Panel */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        {currentPageRegions.map((region) => (
          <div
            key={region.id}
            className={`border  p-4 cursor-pointer transition-colors ${
              activeRegion === region.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200'
            }`}
            onClick={() => setActiveRegion(region.id)}
          >
            <h4 className="font-medium">{region.label}</h4>
            <p className="text-sm text-gray-600">
              Confidence: {(region.confidence * 100).toFixed(1)}%
            </p>
            <p className="text-sm bg-gray-50 p-2  mt-2">
              {region.metadata.extractedText}
            </p>
          </div>
        ))}
      </div>
    </div>
  );
}

Styling & Theming

Customize the overlay appearance to match your application's design system:

Overlay Customization

<HighlightOverlay
  document={document}
  containerSize={pageSize}
  className="custom-overlay"
  style={{
    '--highlight-color': '#3b82f6',
    '--highlight-opacity': '0.3',
    '--border-width': '2px'
  }}
  onRegionClick={(region) => {
    // Handle click with custom styling
  }}
/>

For advanced customization, you can also build your own overlay component using the coordinate data from the ProcessedDocument response.

Frequently Asked Questions

How does automatic coordinate scaling work?

The SDK extracts the original PDF page dimensions during processing and stores them with your results. The HighlightOverlay component automatically calculates scale factors based on your container size, so coordinates always align perfectly regardless of zoom level or container dimensions.

Can I customize the overlay appearance?

Yes! The HighlightOverlay component accepts custom styling through CSS classes and inline styles. You can change colors, borders, opacity, and hover effects. For advanced customization, you can also build your own overlay component using the coordinate data from the ProcessedDocument.

How do I handle multi-page documents?

Each region includes a page number. Filter the regions array by the current page before passing to HighlightOverlay. The SDK automatically handles different page dimensions, so each page scales correctly even if they have different sizes.

What PDF viewers work with this?

Any PDF viewer that gives you control over container dimensions works. We've tested with react-pdf, PDF.js, and custom canvas implementations. The key is knowing your viewer's rendered dimensions to pass as containerSize.

Ready to Build Trust with Visual Proof?

Add interactive overlays to your document viewers and transform user trust with coordinate-backed visual verification.

  • No Credit Card Required
  • Interactive PDF Overlays