npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kopjra/pdf-sentinel

v1.11.4

Published

A React and Preact component library for embedding and interacting with PDF documents using EmbedPDF.

Downloads

207

Readme

pdf-sentinel Documentation

A powerful React/Preact library for viewing and interacting with PDF forms. Built on top of embed-pdf-viewer, pdf-sentinel provides a complete solution for rendering PDF documents with form fields, custom toolbars, internationalization, annotation management, and signature handling.

Table of Contents

Installation

npm install @kopjra/pdf-sentinel

Quick Start

import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";

function MyPdfViewer() {
    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "/path/to/document.pdf",
        name: "My Document",
        usage: { type: "plain" },
    });

    return (
        <div style={{ height: "100vh" }}>
            <PdfSentinelContainer>
                <Toolbar />
                <PdfSentinel />
            </PdfSentinelContainer>
        </div>
    );
}

Features

Core Functionality

  • 📄 PDF Viewing — Render PDF documents with zoom, scroll, and page navigation
  • 📋 Form Fields — Support for text inputs, checkboxes, radio buttons, and signature fields
  • ✍️ Form Validation — Built-in validation with error highlighting and auto-scroll to first error
  • 🎨 Toolbar — Customizable toolbar with zoom, page navigation, undo/redo, and download
  • 🌍 Internationalization — Multi-language support (en, it, es, fr, de) with custom overrides
  • 🔧 Annotations — Full annotation management (add, edit, remove, select) with multiple field types
  • 📝 Signatures — Native conditional signature support with automatic group coloring
  • ↩️ Undo/Redo — History plugin support for annotation operations
  • 📱 Pan Support — Touch device pan support with automatic detection
  • ⬇️ PDF Download — Built-in download capability

Three Usage Modes

  1. plain — View-only PDF rendering
  2. form — Interactive form filling with initial values, readonly fields, conditional signatures
  3. annotation — Freeform annotation placement (email, fullname, phone, date, signature, custom)

API Reference

usePdfSentinel Hook

The main hook for initializing and controlling the PDF viewer.

Signature

function usePdfSentinel(options: PdfSentinelOptions): PdfSentinelResult

Options

interface PdfSentinelOptions {
    /** PDF document URL or ArrayBuffer */
    pdf: string | ArrayBuffer;

    /** Document name/identifier */
    name: string;

    /** Locale code (en, it, es, fr, de) */
    locale?: LocaleType;

    /** Custom translations for any locale */
    translations?: Record<string, Record<string, string>>;

    /** Hide the default toolbar */
    hideToolbar?: boolean;

    /** Callback when document finishes loading */
    onDocumentLoaded?: () => void;

    /** Usage mode — determines how the PDF is rendered and interacted with */
    usage: PlainUsage | FormUsage | AnnotationUsage;
}

Usage Modes

PlainUsage

View-only rendering with no form or annotation interactivity.

interface PlainUsage {
    type: "plain";
}

FormUsage

Interactive form filling mode.

interface FormUsage {
    type: "form";
    /** Initial form field values keyed by field name */
    initialValues?: InitialValuesType;
    /** Conditional signature group configurations */
    conditionalSignatures?: ConditionalSignature[];
    /** Field names to treat as signature fields */
    signatureFieldNames?: string[];
}

AnnotationUsage

Freeform annotation placement mode.

interface AnnotationUsage {
    type: "annotation";
    /** Initial annotations to place, or "readFromDocument" to read existing ones */
    initialAnnotations?: AnnotationField[] | "readFromDocument";
    /** Whether to assign the user's email to created annotations */
    assignEmailToAnnotations?: boolean;
    /** Default size for newly created annotations */
    defaultSize?: { width: number; height: number };
    /** Default annotation field type for new annotations */
    defaultType?: AnnotationFieldType;
    /** Whether to auto-select annotations on creation */
    selectOnCreation?: boolean;
}

Return Value

interface PdfSentinelResult {
    /** Container component that wraps the PDF viewer and toolbar */
    PdfSentinelContainer: React.ComponentType<{ children: React.ReactNode }>;

    /** Toolbar component — accepts optional toolbar customization props */
    Toolbar: React.ComponentType<{
        toolbarButtons?: ToolbarItem[];
        removeButtons?: DefaultButtons[];
    }>;

    /** Main PDF viewer component */
    PdfSentinel: React.ComponentType<{}>;

    /** API access for form and annotation operations */
    api: {
        form: UsePdfFormsResult | null;
        annotation: AnnotationsApi | null;
    };

    /** Current page number (1-indexed) */
    currentPage: number | null;

    /** Total number of pages */
    totalPages: number | null;

    /** Whether PDF is loading */
    loadingPdf: boolean;

    /** Whether there was a loading error */
    loadingError: boolean;

    /** The initialized PDF document buffer (available after load) */
    initializedDocument: ArrayBuffer | null;

    /** PDF engine instance */
    engine: PdfEngine<Blob> | null;

    /** Whether PDF has been closed */
    closedPdf: boolean;

    /** Helper methods for controlling the viewer programmatically */
    helpers: {
        zoomIn: () => void;
        zoomOut: () => void;
        resetZoom: () => void;
        scrollToPage: (pageNumber: number) => void;
        download: () => void;
    };
}

Types

InitialValuesType

type InitialValuesType = Record<string, {
    value?: string | boolean;
    readonly?: boolean;
}>;

Initial form field values. Each key is a field name.

Example:

const initialValues: InitialValuesType = {
    "fullName": { value: "John Doe", readonly: false },
    "signature": { value: true, readonly: true },
    "agreeToTerms": { value: true },
};

ConditionalSignature

interface ConditionalSignature {
    index: string;
    fieldNames: string[];
}

Defines a group of signature fields that are conditional on the same clause.

Example:

const conditionalSignatures: ConditionalSignature[] = [
    { index: "1", fieldNames: ["signature1", "signature2"] },
    { index: "2", fieldNames: ["signature3", "signature4"] },
];

AnnotationFieldType

enum AnnotationFieldType {
    EMAIL = "EMAIL",
    FULLNAME = "FULLNAME",
    PHONE = "PHONE",
    DATE = "DATE",
    SIGNATURE = "SIGNATURE",
    CONDITIONAL_SIGNATURE = "CONDITIONAL_SIGNATURE",
    CUSTOM = "CUSTOM",
}

AnnotationField

interface AnnotationField {
    type: AnnotationFieldType;
    /** Unique annotation identifier */
    id: string;
    originalId?: string;
    /** Display name */
    name?: string;
    /** Description (used for conditional signatures) */
    description?: string;
    /** Whether the annotation is required */
    required?: boolean;
    /** Page number (1-based) */
    page: number;
    /** Bounding rectangle in pixels */
    rect: { x: number; y: number; width: number; height: number };
    /** Assigned email address (if applicable) */
    assignedEmail?: string;
}

DefaultButtons

type DefaultButtons = "zoomIn" | "zoomOut" | "fitWidth" | "download" | "undo" | "redo";

Used to selectively remove built-in toolbar buttons.

Form API

Returned via api.form when using FormUsage.

interface UsePdfFormsResult {
    /** All form fields in the document */
    fields: Field[];
    /** Current field values keyed by field name */
    values: Record<string, string | boolean>;
    /** Set a field value */
    setValue: (name: string, value: string | boolean) => void;
    /** Validate all required fields — returns validity and errors */
    validate: () => { valid: boolean; errors: Record<string, string> };
    /** Reload form fields from the document */
    reload: () => void;
    /** Current validation errors (if any) */
    errors?: Record<string, string>;
    /** Whether errors should be displayed */
    showErrors: boolean;
    /** Scroll to a conditional signature group by index */
    scrollToConditionalSignature: (index: string) => Generator;
}

Field

interface Field {
    name: string;
    type: "text" | "checkbox" | "signature" | "radiobutton";
    pageIndex: number;
    bbox: { x: number; y: number; width: number; height: number };
    radioValue?: string;
    readonly?: boolean;
    required?: boolean;
    conditionalIndex?: string;
}

Annotation API

Returned via api.annotation when using AnnotationUsage.

interface AnnotationsApi {
    /** Get all current annotations */
    getAnnotations: () => Promise<AnnotationField[]>;
    /** Get annotation by ID */
    getAnnotationById: (id: string) => AnnotationField | undefined;
    /** Add a new annotation */
    addAnnotation: (annotation: Partial<AnnotationField>) => void;
    /** Update an existing annotation */
    updateAnnotation: (annotation: AnnotationField) => void;
    /** Remove annotations */
    removeAnnotations: (annotations: AnnotationField[]) => void;
    /** Get annotations for a specific page (1-based) */
    getAnnotationsByPage: (page: number) => Promise<AnnotationField[]>;
    /** Subscribe to annotation changes — returns unsubscribe function */
    subscribe: (listener: (event: SubscribeEvent) => void) => () => void;
    /** Select annotations by ID (null to deselect all) */
    selectAnnotations: (ids: string[] | null) => void;
}

Subscribe events:

// Fired on create, update, or delete
interface SubscribeEventModificationsOptions {
    type: "update" | "create" | "delete";
    annotations: AnnotationField[];
}

// Fired on selection change
interface SubscribeEventSelectionOptions {
    type: "selection";
    selectedAnnotationsId: string[] | null;
}

Toolbar Customization

The Toolbar component accepts optional props for extending or trimming the default toolbar.

<Toolbar
    toolbarButtons={customButtons}
    removeButtons={["download", "redo"]}
/>

ToolbarItem

type ToolbarItem = ToolbarButton | ToolbarSpacer;

ToolbarButton

interface ToolbarButton {
    id: string;
    label?: string;
    icon?: string;           // Font Awesome icon class
    title?: string;          // Tooltip text
    onClick: () => void;
    area: "left" | "right";
    disabled?: boolean;
    type: "button";
    primary?: boolean;
}

ToolbarSpacer

interface ToolbarSpacer {
    id: string;
    type: "spacer";
    area: "left" | "right";
}

Internationalization

Supported Locales

  • en — English (default)
  • it — Italian
  • es — Spanish
  • fr — French
  • de — German

Default Translation Keys

{
    zoomIn: string;
    zoomOut: string;
    resetZoom: string;
    download: string;
    pageNumberTitle: string;
    signature: string;
    conditional1: string;   // "By accepting clause %s"
    conditional2: string;   // "your signature will be placed here"
}

Custom Translations

Override or add translations for any locale:

const translations = {
    en: {
        zoomIn: "Enlarge",
        download: "Download PDF",
        customKey: "Custom value",
    },
    it: {
        customKey: "Valore personalizzato",
    },
};

Examples

Basic PDF Viewer (Plain Mode)

import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";

export function BasicViewer() {
    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "https://example.com/document.pdf",
        name: "Document",
        usage: { type: "plain" },
    });

    return (
        <div style={{ height: "600px" }}>
            <PdfSentinelContainer>
                <Toolbar />
                <PdfSentinel />
            </PdfSentinelContainer>
        </div>
    );
}

PDF with Form Fields

import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";

export function FormViewer() {
    const { PdfSentinelContainer, Toolbar, PdfSentinel, api } = usePdfSentinel({
        pdf: "/document.pdf",
        name: "Form Document",
        usage: {
            type: "form",
            initialValues: {
                "firstName": { value: "John" },
                "lastName": { value: "Doe" },
            },
        },
    });

    const handleValidate = () => {
        const result = api.form?.validate();
        if (result?.valid) {
            console.log("Form is valid, values:", api.form?.values);
        } else {
            console.log("Errors:", result?.errors);
        }
    };

    return (
        <>
            <div style={{ height: "600px" }}>
                <PdfSentinelContainer>
                    <Toolbar />
                    <PdfSentinel />
                </PdfSentinelContainer>
            </div>
            <button onClick={handleValidate}>Validate</button>
        </>
    );
}

Conditional Signatures

import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";

export function SignatureViewer() {
    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "/contract.pdf",
        name: "Contract",
        usage: {
            type: "form",
            conditionalSignatures: [
                { index: "1", fieldNames: ["authorizedBy", "authorizedDate"] },
                { index: "2", fieldNames: ["approvedBy", "approvedDate"] },
            ],
        },
    });

    return (
        <div style={{ height: "600px" }}>
            <PdfSentinelContainer>
                <Toolbar />
                <PdfSentinel />
            </PdfSentinelContainer>
        </div>
    );
}

Annotation Mode

import { usePdfSentinel, AnnotationFieldType } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";

export function AnnotationEditor() {
    const { PdfSentinelContainer, Toolbar, PdfSentinel, api } = usePdfSentinel({
        pdf: "/document.pdf",
        name: "Annotate",
        usage: {
            type: "annotation",
            initialAnnotations: "readFromDocument",
            defaultType: AnnotationFieldType.SIGNATURE,
            defaultSize: { width: 200, height: 60 },
            selectOnCreation: true,
        },
    });

    const handleExport = async () => {
        const annotations = await api.annotation?.getAnnotations();
        console.log("Annotations:", annotations);
    };

    // Subscribe to annotation changes
    api.annotation?.subscribe((event) => {
        if (event.type === "create") {
            console.log("New annotations:", event.annotations);
        }
    });

    return (
        <>
            <div style={{ height: "600px" }}>
                <PdfSentinelContainer>
                    <Toolbar />
                    <PdfSentinel />
                </PdfSentinelContainer>
            </div>
            <button onClick={handleExport}>Export Annotations</button>
        </>
    );
}

Custom Toolbar Buttons

import { usePdfSentinel, ToolbarItem } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";

export function CustomToolbar() {
    const customButtons: ToolbarItem[] = [
        {
            id: "print-btn",
            label: "Print",
            icon: "fal fa-print",
            title: "Print document",
            area: "left",
            type: "button",
            onClick: () => window.print(),
        },
        {
            id: "spacer-1",
            type: "spacer",
            area: "left",
        },
        {
            id: "share-btn",
            label: "Share",
            icon: "fal fa-share",
            title: "Share document",
            area: "right",
            type: "button",
            onClick: () => console.log("Share clicked"),
        },
    ];

    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "/document.pdf",
        name: "Document",
        usage: { type: "plain" },
    });

    return (
        <div style={{ height: "600px" }}>
            <PdfSentinelContainer>
                <Toolbar
                    toolbarButtons={customButtons}
                    removeButtons={["undo", "redo"]}
                />
                <PdfSentinel />
            </PdfSentinelContainer>
        </div>
    );
}

Multi-Language Support

import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";
import { useState } from "react";

export function MultiLanguageViewer() {
    const [locale, setLocale] = useState<"en" | "it" | "es" | "fr" | "de">("en");

    const customTranslations = {
        en: { download: "Download PDF" },
        it: { download: "Scarica PDF" },
    };

    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "/document.pdf",
        name: "Document",
        locale,
        translations: customTranslations,
        usage: { type: "plain" },
    });

    return (
        <div>
            <select onChange={(e) => setLocale(e.target.value as any)} value={locale}>
                <option value="en">English</option>
                <option value="it">Italiano</option>
                <option value="es">Español</option>
                <option value="fr">Français</option>
                <option value="de">Deutsch</option>
            </select>
            <div style={{ height: "600px", marginTop: "10px" }}>
                <PdfSentinelContainer>
                    <Toolbar />
                    <PdfSentinel />
                </PdfSentinelContainer>
            </div>
        </div>
    );
}

External Toolbar (Custom UI)

import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";

export function ExternalToolbarViewer() {
    const {
        PdfSentinelContainer,
        PdfSentinel,
        currentPage,
        totalPages,
        helpers,
    } = usePdfSentinel({
        pdf: "/document.pdf",
        name: "Document",
        hideToolbar: true,
        usage: { type: "plain" },
    });

    return (
        <div>
            {/* Custom toolbar outside the container */}
            <div style={{ display: "flex", gap: "8px", padding: "8px" }}>
                <button onClick={helpers.zoomIn}>+</button>
                <button onClick={helpers.zoomOut}>−</button>
                <button onClick={helpers.resetZoom}>Fit</button>
                <span>{currentPage} / {totalPages}</span>
                <button onClick={helpers.download}>Download</button>
            </div>

            <div style={{ height: "600px" }}>
                <PdfSentinelContainer>
                    <PdfSentinel />
                </PdfSentinelContainer>
            </div>
        </div>
    );
}

Using React and Preact Variants

To use the Preact variant, import from @kopjra/pdf-sentinel/preact instead of @kopjra/pdf-sentinel. The React variant is the default; you can also use @kopjra/pdf-sentinel/react for clarity.

// React variant (default)
import { usePdfSentinel } from "@kopjra/pdf-sentinel";

// React variant (explicit)
import { usePdfSentinel } from "@kopjra/pdf-sentinel/react";

// Preact variant
import { usePdfSentinel } from "@kopjra/pdf-sentinel/preact";

The same code works for both React and Preact — the library automatically uses the correct framework variant based on the import path.

Browser Support

  • Chrome/Chromium (latest)
  • Firefox (latest)
  • Safari (latest)
  • Edge (latest)

Contributing

Contributions are welcome! Please ensure:

  1. TypeScript types are correctly defined
  2. All code follows the project style guide
  3. npm run typecheck passes
  4. Changes are tested in both React and Preact variants

License

ISC