This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /blog/pdfjs-annotation-editor-layer.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. How to use the PDF.js AnnotationEditorLayer for native annotation editing

Table of contents

    Since PDF.js 3.x, the built-in AnnotationEditorLayer provides native support for creating and editing annotations — free text, ink (drawing), stamps (images), and highlights — without any third-party libraries. This tutorial walks through enabling, customizing, and saving annotations.
    TL;DR
    • Enable PDF.js’s built-in AnnotationEditorLayer for free text, ink, stamp, and highlight annotations.
    • Switch between editor tools via switchannotationeditormode and tune defaults with switchannotationeditorparams.
    • Save the edited PDF (annotations included) with pdfDocument.saveDocument().

    Prerequisites

    This guide assumes you’ve already wired up a PDF.js viewer — PDFViewer, EventBus, PDFLinkService, and a loaded pdfDocument. If you haven’t, start with our guide on how to set up a custom PDF.js viewer in React.

    You’ll also need:

    • pdfjs-dist 4.x or later (HIGHLIGHT editor landed late in the 3.x line; 4.x is the safe baseline)
    • pdfjs-dist/web/pdf_viewer.css imported so the editor layer renders correctly

    What it provides

    Annotation typeDescription
    Free textEditable text boxes placed anywhere on the page
    InkFreehand drawing (pen tool)
    StampImage annotations dropped onto pages
    HighlightNative text highlight annotations

    Enabling the AnnotationEditor

    When creating your PDFViewer, pass the AnnotationEditorMode:

    const pdfjs = await import("pdfjs-dist/web/pdf_viewer.mjs");
    const viewer = new pdfjs.PDFViewer({
    container: document.getElementById("pdf-container"),
    eventBus,
    linkService,
    findController,
    annotationEditorMode: pdfjs.AnnotationEditorType.NONE, // Start with editing disabled.
    });

    Switching editor modes

    Toggle between annotation types by dispatching switchannotationeditormode on the EventBus:

    // Enable free text editor.
    eventBus.dispatch("switchannotationeditormode", {
    source: this,
    mode: pdfjs.AnnotationEditorType.FREETEXT,
    });
    // Enable ink (drawing) editor.
    eventBus.dispatch("switchannotationeditormode", {
    source: this,
    mode: pdfjs.AnnotationEditorType.INK,
    });
    // Enable stamp (image) editor.
    eventBus.dispatch("switchannotationeditormode", {
    source: this,
    mode: pdfjs.AnnotationEditorType.STAMP,
    });
    // Enable highlight editor.
    eventBus.dispatch("switchannotationeditormode", {
    source: this,
    mode: pdfjs.AnnotationEditorType.HIGHLIGHT,
    });
    // Disable all editors (return to view mode).
    eventBus.dispatch("switchannotationeditormode", {
    source: this,
    mode: pdfjs.AnnotationEditorType.NONE,
    });

    AnnotationEditorType enum

    The numeric values used by switchannotationeditormode are non-sequential because the enum grew across PDF.js releases:

    const AnnotationEditorType = {
    DISABLE: -1,
    NONE: 0,
    FREETEXT: 3,
    STAMP: 13,
    INK: 15,
    HIGHLIGHT: 9,
    };

    Customizing free text annotations

    You can set default properties for new free text annotations via the EventBus:

    // Set default font size.
    eventBus.dispatch("switchannotationeditorparams", {
    source: this,
    type: pdfjs.AnnotationEditorParamsType.FREETEXT_SIZE,
    value: 14,
    });
    // Set default font color.
    eventBus.dispatch("switchannotationeditorparams", {
    source: this,
    type: pdfjs.AnnotationEditorParamsType.FREETEXT_COLOR,
    value: "#FF0000",
    });

    Customizing ink annotations

    Use switchannotationeditorparams to set the color, thickness, and opacity for new ink strokes:

    // Set ink color.
    eventBus.dispatch("switchannotationeditorparams", {
    source: this,
    type: pdfjs.AnnotationEditorParamsType.INK_COLOR,
    value: "#0000FF",
    });
    // Set ink thickness.
    eventBus.dispatch("switchannotationeditorparams", {
    source: this,
    type: pdfjs.AnnotationEditorParamsType.INK_THICKNESS,
    value: 3,
    });
    // Set ink opacity.
    eventBus.dispatch("switchannotationeditorparams", {
    source: this,
    type: pdfjs.AnnotationEditorParamsType.INK_OPACITY,
    value: 80, // 0-100
    });

    Customizing highlight annotations

    Highlight annotations support one parameter — color:

    // Set highlight color.
    eventBus.dispatch("switchannotationeditorparams", {
    source: this,
    type: pdfjs.AnnotationEditorParamsType.HIGHLIGHT_COLOR,
    value: "#FFFF00",
    });

    Saving annotations

    The built-in editor layer can serialize all annotations directly into the PDF. Use PDFDocumentProxy.saveDocument():

    // Save the PDF with all editor annotations baked in.
    const data = await pdfDocument.saveDocument();
    // data is a Uint8Array of the modified PDF.
    // Download it.
    const blob = new Blob([data], { type: "application/pdf" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "annotated.pdf";
    a.click();
    URL.revokeObjectURL(url);

    Because the editor layer ships with PDF.js, you no longer need a separate library to embed annotations into the saved file.

    React toolbar example

    A minimal React toolbar that wires buttons to switchannotationeditormode and tracks the active tool in state:

    function AnnotationEditorToolbar() {
    const { eventBus } = useContext(PDFContext);
    const [activeMode, setActiveMode] = useState("NONE");
    const setMode = (mode) => {
    const modeValue = pdfjs.AnnotationEditorType[mode];
    eventBus.dispatch("switchannotationeditormode", {
    source: null,
    mode: modeValue,
    });
    setActiveMode(mode);
    };
    return (
    <div>
    <button
    className={activeMode === "FREETEXT" ? "active" : ""}
    onClick={() => setMode("FREETEXT")}
    >
    Text
    </button>
    <button
    className={activeMode === "INK" ? "active" : ""}
    onClick={() => setMode("INK")}
    >
    Draw
    </button>
    <button
    className={activeMode === "HIGHLIGHT" ? "active" : ""}
    onClick={() => setMode("HIGHLIGHT")}
    >
    Highlight
    </button>
    <button
    className={activeMode === "STAMP" ? "active" : ""}
    onClick={() => setMode("STAMP")}
    >
    Image
    </button>
    <button onClick={() => setMode("NONE")}>
    Done
    </button>
    </div>
    );
    }

    When to use built-in vs. custom annotations

    ScenarioRecommendation
    Simple highlighting + free textBuilt-in AnnotationEditorLayer
    Collaborative annotations with commentsCustom layer (need per-annotation metadata, authors, threads)
    Annotations synchronized to a databaseCustom layer (built-in doesn’t expose annotation events for persistence)
    PDF export with annotationsBuilt-in saveDocument()
    Quick markup for personal useBuilt-in AnnotationEditorLayer

    CSS

    The annotation editor layer requires its own CSS:

    import "pdfjs-dist/web/pdf_viewer.css";

    The default CSS includes styles for .annotationEditorLayer, editor toolbars, and annotation handles.

    Key points

    • AnnotationEditorLayer has been built into PDF.js since version 3.x — no extra dependencies.
    • Switch modes by dispatching switchannotationeditormode on the EventBus.
    • Customize colors, sizes, and opacity via switchannotationeditorparams.
    • pdfDocument.saveDocument() serializes all annotations into the PDF — no separate library required.
    • The built-in layer handles all coordinate conversion, scaling, and rotation automatically.
    • For complex workflows (comments, collaboration, and database synchronization), a custom layer still makes sense.
    • For simple markup and export, the built-in layer is significantly less code.

    How Nutrient Web SDK handles this

    Nutrient Web SDK ships its annotation tools as part of the viewer rather than as a separate editor layer you opt into. Switching tools is a single setViewState call, and annotation events (create, update, delete) are exposed on the instance for autosave and collaboration.

    const instance = await NutrientViewer.load({
    container: "#pdf-container",
    document: "document.pdf",
    });
    instance.setViewState((v) =>
    v.set("interactionMode", NutrientViewer.InteractionMode.INK),
    );
    instance.addEventListener("annotations.create", (annotations) => {
    // Persist to your backend or synchronize via Instant.
    });

    Compared to the built-in editor layer’s four types (free text, ink, stamp, highlight), Nutrient provides 17+ annotation types with comment threads, @mentions, and real-time synchronization via Instant. Annotation export and embedding are handled by the SDK.

    FAQ

    Which PDF.js version do I need for the AnnotationEditorLayer?

    The editor layer landed in PDF.js 3.x, but HIGHLIGHT arrived later in that line. Use pdfjs-dist 4.x or later for the full set of free text, ink, stamp, and highlight annotation types.

    How do I detect when annotations change so I can autosave?

    The editor layer doesn’t expose per-annotation events directly. The two practical hooks are (1) the annotationeditorstateschanged EventBus event for tool-state transitions, and (2) calling pdfDocument.saveDocument() on a debounced timer or on beforeunload. For real per-annotation create/update/delete events, you need a custom annotation layer.

    Can I preload existing annotations into the editor?

    Annotations already embedded in the PDF are read and shown by the editor layer when the document loads. There’s no public API to inject annotations programmatically — you’d have to write them into the PDF first (using pdf-lib or similar) and then open the modified file in PDF.js.

    What’s the difference between AnnotationEditorType.NONE and DISABLE?

    NONE (0) keeps the editor layer mounted but with no tool active — users can still click existing annotations to edit them. DISABLE (-1) removes the editor layer entirely, putting the viewer in read-only mode.

    Does saveDocument() produce a flat PDF or one with editable annotations?

    It produces a PDF with the new annotations encoded as standard PDF annotation objects (FreeText, Ink, Stamp, Highlight). Other PDF viewers can read and continue editing them — they’re not rasterized.

    How do I customize the toolbar UI?

    PDF.js’s default viewer UI exposes a toolbar; if you’re using your own (the React example above), call eventBus.dispatch("switchannotationeditormode", ...) and "switchannotationeditorparams" from your own buttons. There’s no built-in component to drop into a custom layout.


    See Nutrient Web SDK for built-in annotations with comment threads and real-time synchronization, follow the migration guide to switch from PDF.js, or talk to Sales about your requirements.

    Austin Nguyen

    Austin Nguyen

    AI Engineer

    When Austin isn’t pulling all-nighters to build new features, he enjoys watching science videos on YouTube and cooking.

    Explore related topics

    Try for free Ready to get started?