Creating and filling PDF forms programmatically in JavaScript

Table of contents

    Creating and filling PDF forms programmatically in JavaScript
    Summary

    Create PDF forms with text fields, checkboxes, radio buttons, and signatures using Nutrient Web SDK. Fill forms programmatically from Instant JSON, XFDF, or a database.

    PDF forms automate data collection for applications, contracts, surveys, and onboarding workflows. This tutorial shows how to create form fields programmatically and populate them with data.

    What you’ll build:

    • Text inputs, checkboxes, radio buttons, and a signature field
    • Prefilled forms using JSON or XFDF
    • Database-driven form population

    Display a PDF

    1. Create a project directory:

      Terminal window
      mkdir CreatingPdfForms
      cd CreatingPdfForms
      npm init -y
    2. Install Nutrient and Vite:

      Terminal window
      npm install @nutrient-sdk/viewer
      npm install -D vite

    The useCDN: true option loads SDK assets from Nutrient's CDN, so no manual asset copying is required.

    1. Add a PDF document named document.pdf to your project’s root directory.

      image showing a screenshot of the pdf and its contents

    2. Create index.html and index.js:

      Terminal window
      touch index.html index.js
    3. Add this to index.js:

      (async () => {
      const NutrientViewer = (await import("@nutrient-sdk/viewer")).default;
      const container = document.getElementById("pspdfkit");
      NutrientViewer.load({
      container,
      document: "document.pdf",
      useCDN: true,
      })
      .then((instance) => {
      console.log("Nutrient loaded", instance);
      })
      .catch((error) => {
      console.error(error.message);
      });
      })();
    4. Add this to index.html:

      <!DOCTYPE html>
      <html>
      <head>
      <title>PDF Forms</title>
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      </head>
      <body>
      <div id="pspdfkit" style="width: 100%; height: 100vh;"></div>
      <script type="module" src="index.js"></script>
      </body>
      </html>
    5. Start the dev server:

      Terminal window
      npx vite

    Add form fields

    Each form field needs two parts: a widget annotation (position and size) and a form field (type and behavior).

    The widget annotation defines where the field appears:

    const firstNameWidget = new NutrientViewer.Annotations.WidgetAnnotation({
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: "First Name",
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 115,
    top: 98,
    width: 200,
    height: 25,
    }),
    });

    The form field defines the field type and links to the widget:

    const firstNameFormField = new NutrientViewer.FormFields.TextFormField({
    name: "First Name",
    annotationIds: new NutrientViewer.Immutable.List([firstNameWidget.id]),
    });

    Create both together:

    await instance.create([firstNameWidget, firstNameFormField]);

    This is the signature field:

    const signatureWidget = new NutrientViewer.Annotations.WidgetAnnotation({
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 43,
    top: 325,
    width: 150,
    height: 75,
    }),
    formFieldName: "Signature",
    });
    const signatureFormField = new NutrientViewer.FormFields.SignatureFormField({
    name: "Signature",
    annotationIds: new NutrientViewer.Immutable.List([signatureWidget.id]),
    });
    await instance.create([signatureWidget, signatureFormField]);

    Radio buttons and checkboxes use multiple widget annotations for different values:

    // Radio button widgets.
    const yesRadioWidget = new NutrientViewer.Annotations.WidgetAnnotation({
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: "Human",
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 80, top: 188, width: 20, height: 20,
    }),
    });
    const noRadioWidget = new NutrientViewer.Annotations.WidgetAnnotation({
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: "Human",
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 80, top: 214, width: 20, height: 20,
    }),
    });
    const maybeRadioWidget = new NutrientViewer.Annotations.WidgetAnnotation({
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: "Human",
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 80, top: 240, width: 20, height: 20,
    }),
    });
    const radioFormField = new NutrientViewer.FormFields.RadioButtonFormField({
    name: "Human",
    annotationIds: new NutrientViewer.Immutable.List([
    yesRadioWidget.id,
    noRadioWidget.id,
    maybeRadioWidget.id,
    ]),
    options: new NutrientViewer.Immutable.List([
    new NutrientViewer.FormOption({ label: "Yes", value: "1" }),
    new NutrientViewer.FormOption({ label: "No", value: "2" }),
    new NutrientViewer.FormOption({ label: "Maybe", value: "3" }),
    ]),
    defaultValue: "Maybe",
    });
    // Checkbox
    const checkBoxWidget = new NutrientViewer.Annotations.WidgetAnnotation({
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: "Fun",
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 128, top: 269.5, width: 20, height: 20,
    }),
    });
    const checkBoxFormField = new NutrientViewer.FormFields.CheckBoxFormField({
    name: "Fun",
    annotationIds: new NutrientViewer.Immutable.List([checkBoxWidget.id]),
    options: new NutrientViewer.Immutable.List([
    new NutrientViewer.FormOption({ label: "FunCheck", value: "1" }),
    ]),
    });
    await instance.create([
    yesRadioWidget, noRadioWidget, maybeRadioWidget, radioFormField,
    checkBoxWidget, checkBoxFormField,
    ]);

    Here’s a complete example for creating all form fields:

    index.js
    (async () => {
    const NutrientViewer = (await import("@nutrient-sdk/viewer")).default;
    const container = document.getElementById("pspdfkit");
    NutrientViewer.load({
    container,
    document: "document.pdf",
    useCDN: true,
    })
    .then(async (instance) => {
    console.log("Nutrient loaded", instance);
    164 collapsed lines
    // Creating the first name text form field.
    const firstNameWidget = new NutrientViewer.Annotations.WidgetAnnotation(
    {
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: 'First Name',
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 115,
    top: 98,
    width: 200,
    height: 25,
    }),
    },
    );
    const firstNameFormField = new NutrientViewer.FormFields.TextFormField({
    name: 'First Name',
    annotationIds: new NutrientViewer.Immutable.List([
    firstNameWidget.id,
    ]),
    });
    // Creating the last name text form field.
    const lastNameWidget = new NutrientViewer.Annotations.WidgetAnnotation({
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: 'Last Name',
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 115,
    top: 128,
    width: 200,
    height: 25,
    }),
    });
    const lastNameFormField = new NutrientViewer.FormFields.TextFormField({
    name: 'Last Name',
    annotationIds: new NutrientViewer.Immutable.List([
    lastNameWidget.id,
    ]),
    });
    // Creating a new radio button form field.
    const yesRadioWidget = new NutrientViewer.Annotations.WidgetAnnotation({
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: 'Human',
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 80,
    top: 188,
    width: 20,
    height: 20,
    }),
    });
    const noRadioWidget = new NutrientViewer.Annotations.WidgetAnnotation({
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: 'Human',
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 80,
    top: 214,
    width: 20,
    height: 20,
    }),
    });
    const maybeRadioWidget = new NutrientViewer.Annotations.WidgetAnnotation(
    {
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: 'Human',
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 80,
    top: 240,
    width: 20,
    height: 20,
    }),
    },
    );
    const radioFormField = new NutrientViewer.FormFields.RadioButtonFormField(
    {
    name: 'Human',
    annotationIds: new NutrientViewer.Immutable.List([
    yesRadioWidget.id,
    noRadioWidget.id,
    maybeRadioWidget.id,
    ]),
    options: new NutrientViewer.Immutable.List([
    new NutrientViewer.FormOption({
    label: 'Yes',
    value: '1',
    }),
    new NutrientViewer.FormOption({
    label: 'No',
    value: '2',
    }),
    new NutrientViewer.FormOption({
    label: 'Maybe',
    value: '3',
    }),
    ]),
    defaultValue: 'Maybe',
    },
    );
    // Creating a new checkbox form field.
    const checkBoxWidget = new NutrientViewer.Annotations.WidgetAnnotation({
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    formFieldName: 'Fun',
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 128,
    top: 269.5,
    width: 20,
    height: 20,
    }),
    });
    const checkBoxFormField = new NutrientViewer.FormFields.CheckBoxFormField(
    {
    name: 'Fun',
    annotationIds: new NutrientViewer.Immutable.List([
    checkBoxWidget.id,
    ]),
    options: new NutrientViewer.Immutable.List([
    new NutrientViewer.FormOption({
    label: 'FunCheck',
    value: '1',
    }),
    ]),
    },
    );
    // Creating a new signature form field.
    const signatureWidget = new NutrientViewer.Annotations.WidgetAnnotation(
    {
    id: NutrientViewer.generateInstantId(),
    pageIndex: 0,
    boundingBox: new NutrientViewer.Geometry.Rect({
    left: 43,
    top: 325,
    width: 150,
    height: 75,
    }),
    formFieldName: 'Signature',
    },
    );
    const signatureFormField = new NutrientViewer.FormFields.SignatureFormField(
    {
    name: 'Signature',
    annotationIds: new NutrientViewer.Immutable.List([
    signatureWidget.id,
    ]),
    },
    );
    await instance.create([
    firstNameWidget,
    firstNameFormField,
    lastNameWidget,
    lastNameFormField,
    yesRadioWidget,
    noRadioWidget,
    maybeRadioWidget,
    radioFormField,
    checkBoxWidget,
    checkBoxFormField,
    signatureWidget,
    signatureFormField,
    ]);
    })
    .catch((error) => {
    console.error(error.message);
    });
    })();

    Form field creation result

    Now, export the resulting document and use it in the next section to fill out the form fields. To do that, add the following after instance.create();:

    instance.exportPDF().then((buffer) => {
    const blob = new Blob([buffer], { type: "application/pdf" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "document.pdf";
    a.click();
    URL.revokeObjectURL(url);
    });

    Refresh your app in the browser to get a copy of the document with the form fields.

    gif showing the result of 'instance.exportPDF()'

    Fill forms with Instant JSON

    Now that you have a form with fields, let's explore how to populate them programmatically with data.

    Use the exported PDF from the previous section. First, check what fields exist:

    const formFieldValues = instance.getFormFieldValues();
    console.log(formFieldValues);

    image showing the result of 'console.log(formFieldValues);'

    Keys are field names, and values are null, string, or Array<string>.

    Pass form values in the instantJSON option:

    instantJSON: {
    format: "https://pspdfkit.com/instant-json/v1",
    formFieldValues: [
    {
    name: "First Name",
    value: "John",
    type: "pspdfkit/form-field-value",
    v: 1,
    },
    {
    name: "Last Name",
    value: "Appleseed",
    type: "pspdfkit/form-field-value",
    v: 1,
    },
    {
    name: "Human",
    value: "3",
    type: "pspdfkit/form-field-value",
    v: 1,
    },
    {
    name: "Fun",
    value: "1",
    type: "pspdfkit/form-field-value",
    v: 1,
    },
    ],
    }

    Signature fields require an ink annotation instead of a value. The code below draws a simple “X” signature by creating two diagonal lines within the signature field's boundaries:

    // The name of the signature field you want.
    //
    const formFieldName = 'Signature';
    // First, get all `FormFields` in the `Document`.
    //
    const formFields = await instance.getFormFields();
    // Get a signature form with the specific name you want.
    //
    const field = formFields.find(
    (formField) =>
    formField.name === formFieldName &&
    formField instanceof NutrientViewer.FormFields.SignatureFormField,
    );
    // In this example, assume the widget you need is on the first page.
    //
    const annotations = await instance.getAnnotations(0);
    // Find that widget.
    //
    const widget = annotations.find(
    (annotation) =>
    annotation instanceof NutrientViewer.Annotations.WidgetAnnotation &&
    annotation.formFieldName === field.name,
    );
    // Make a new ink annotation.
    //
    const annotation = new NutrientViewer.Annotations.InkAnnotation({
    pageIndex: 0,
    lines: NutrientViewer.Immutable.List([
    NutrientViewer.Immutable.List([
    new NutrientViewer.Geometry.DrawingPoint({
    x: widget.boundingBox.left + 10,
    y: widget.boundingBox.top + 10,
    }),
    new NutrientViewer.Geometry.DrawingPoint({
    x: widget.boundingBox.left + widget.boundingBox.width - 10,
    y: widget.boundingBox.top + widget.boundingBox.height - 10,
    }),
    ]),
    NutrientViewer.Immutable.List([
    new NutrientViewer.Geometry.DrawingPoint({
    x: widget.boundingBox.left + widget.boundingBox.width - 10,
    y: widget.boundingBox.top + 10,
    }),
    new NutrientViewer.Geometry.DrawingPoint({
    x: widget.boundingBox.left + 10,
    y: widget.boundingBox.top + widget.boundingBox.height - 10,
    }),
    ]),
    ]),
    boundingBox: widget.boundingBox,
    isSignature: true,
    });
    instance.create(annotation);

    Here’s a complete example for filling form fields and adding a signature:

    index.js
    (async () => {
    const NutrientViewer = (await import("@nutrient-sdk/viewer")).default;
    const container = document.getElementById("pspdfkit");
    NutrientViewer.load({
    container,
    document: "document.pdf",
    useCDN: true,
    instantJSON: {
    format: "https://pspdfkit.com/instant-json/v1",
    formFieldValues: [
    { name: "First Name", value: "John", type: "pspdfkit/form-field-value", v: 1 },
    { name: "Last Name", value: "Appleseed", type: "pspdfkit/form-field-value", v: 1 },
    { name: "Human", value: "3", type: "pspdfkit/form-field-value", v: 1 },
    { name: "Fun", value: "1", type: "pspdfkit/form-field-value", v: 1 },
    ],
    },
    })
    .then(async (instance) => {
    console.log("Nutrient loaded", instance);
    31 collapsed lines
    // Get the signature field.
    const formFields = await instance.getFormFields();
    const field = formFields.find(
    (f) => f.name === "Signature" && f instanceof NutrientViewer.FormFields.SignatureFormField
    );
    if (!field) return;
    // Get the widget annotation.
    const annotations = await instance.getAnnotations(0);
    const widget = annotations.find(
    (a) => a instanceof NutrientViewer.Annotations.WidgetAnnotation && a.formFieldName === field.name
    );
    if (!widget) return;
    // Create an ink annotation as the signature.
    const annotation = new NutrientViewer.Annotations.InkAnnotation({
    pageIndex: 0,
    lines: NutrientViewer.Immutable.List([
    NutrientViewer.Immutable.List([
    new NutrientViewer.Geometry.DrawingPoint({ x: widget.boundingBox.left + 10, y: widget.boundingBox.top + 10 }),
    new NutrientViewer.Geometry.DrawingPoint({ x: widget.boundingBox.left + widget.boundingBox.width - 10, y: widget.boundingBox.top + widget.boundingBox.height - 10 }),
    ]),
    NutrientViewer.Immutable.List([
    new NutrientViewer.Geometry.DrawingPoint({ x: widget.boundingBox.left + widget.boundingBox.width - 10, y: widget.boundingBox.top + 10 }),
    new NutrientViewer.Geometry.DrawingPoint({ x: widget.boundingBox.left + 10, y: widget.boundingBox.top + widget.boundingBox.height - 10 }),
    ]),
    ]),
    boundingBox: widget.boundingBox,
    isSignature: true,
    });
    instance.create(annotation);
    })
    .catch((error) => {
    console.error(error.message);
    });
    })();

    Form fields filled with Instant JSON

    Fill forms with XFDF

    XFDF is an XML format for form data:

    (async () => {
    const NutrientViewer = (await import("@nutrient-sdk/viewer")).default;
    const container = document.getElementById("pspdfkit");
    const XFDF = `<?xml version="1.0" encoding="UTF-8"?>
    <xfdf xml:space="preserve" xmlns="http://ns.adobe.com/xfdf/">
    <annots></annots>
    <fields>
    <field name="First Name"><value>John</value></field>
    <field name="Last Name"><value>Appleseed</value></field>
    <field name="Human"><value>3</value></field>
    <field name="Fun"><value>1</value></field>
    </fields>
    </xfdf>`;
    NutrientViewer.load({
    container,
    document: "document.pdf",
    useCDN: true,
    XFDF,
    });
    })();

    Form fields filled with XFDF

    Fill forms from a database

    Fetch user data from your API and convert it to Instant JSON:

    // Example response from `/user` endpoint.
    { "firstName": "John", "lastName": "Appleseed", "human": "3", "fun": "1" }
    (async () => {
    const NutrientViewer = (await import("@nutrient-sdk/viewer")).default;
    const container = document.getElementById("pspdfkit");
    // Fetch user data from your server.
    const response = await fetch("https://[YOUR-SERVER]/user");
    const { firstName, lastName, human, fun } = await response.json();
    // Convert to Instant JSON format.
    const instantJSON = {
    format: "https://pspdfkit.com/instant-json/v1",
    formFieldValues: [
    { v: 1, type: "pspdfkit/form-field-value", name: "First Name", value: firstName },
    { v: 1, type: "pspdfkit/form-field-value", name: "Last Name", value: lastName },
    { v: 1, type: "pspdfkit/form-field-value", name: "Human", value: human },
    { v: 1, type: "pspdfkit/form-field-value", name: "Fun", value: fun },
    ],
    };
    NutrientViewer.load({
    container,
    document: "document.pdf",
    useCDN: true,
    instantJSON,
    });
    })();

    Form fields filled from database

    FAQ

    What form field types does Nutrient support?

    Nutrient supports text fields, checkboxes, radio buttons, signature fields, combo boxes (dropdowns), and list boxes. Each type has a corresponding class like TextFormField, CheckBoxFormField, and SignatureFormField.

    How do I read form field values?

    Use instance.getFormFieldValues() to get all values as an object where keys are field names and values are the current entries. For individual fields, use instance.getFormFields() to access field properties.

    What’s the difference between Instant JSON and XFDF?

    Instant JSON is Nutrient’s native format — it’s compact and supports all Nutrient features, including annotations. XFDF is an Adobe standard XML format with broader compatibility across PDF tools. Use Instant JSON for Nutrient-only workflows and XFDF when exchanging data with other PDF software.

    Can I make form fields required?

    Yes. Set the required property to true when creating a form field:

    const field = new NutrientViewer.FormFields.TextFormField({
    name: "Email",
    required: true,
    annotationIds: new NutrientViewer.Immutable.List([widget.id]),
    });
    How do I export a filled PDF form?

    Use instance.exportPDF() to get the PDF as an ArrayBuffer. You can then create a download link or send it to your server:

    const buffer = await instance.exportPDF();
    const blob = new Blob([buffer], { type: "application/pdf" });

    Conclusion

    You now have a PDF form with text fields, checkboxes, radio buttons, and signatures that can be prefilled from JSON, XFDF, or your database. Use this to build document workflows, onboarding forms, or contract signing apps.

    Try the forms demo or explore the forms documentation.

    Teja Tatimatla

    Teja Tatimatla

    Hulya Masharipov

    Hulya Masharipov

    Technical Writer

    Hulya is a frontend web developer and technical writer who enjoys creating responsive, scalable, and maintainable web experiences. She’s passionate about open source, web accessibility, cybersecurity privacy, and blockchain.

    Explore related topics

    FREE TRIAL Ready to get started?