---
title: "Filling forms — React"
description: "Let people fill in PDF forms, read and set values from your code, check required fields, and move form data in and out."
framework: "React"
source: "https://www.embedpdf.com/docs/headless/react/forms"
---

# Filling forms

Many PDFs are forms: text boxes, checkboxes, dropdowns, signature fields. Register `formPlugin()` and
put `<FormLayer>` in each page, and people can fill them in:

**`form-layer.tsx`**

```tsx
import { FormLayer, formPlugin } from '@embedpdf/react/form';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { Viewer } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { engine } from './pdf';

const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

export function FormViewer() {
  return (
    <Viewer engine={engine} plugins={plugins}>
      <Stage>
        {() => (
          <>
            <RenderLayer />
            <FormLayer />
          </>
        )}
      </Stage>
    </Viewer>
  );
}
```

**`basic.tsx`**

```tsx
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { FormLayer, formPlugin, useForm, useFormState } from '@embedpdf/react/form';
import { localEngine } from '@embedpdf/engine';

import './basic.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// How the new fields look: part of the PDF, like the rest of the page.
const look = { color: '#94a3b8', interiorColor: '#f8fafc', strokeWidth: 1, fontSize: 11 };

/** The ebook has no form, so this adds one to its last page when it opens, and goes there. */
function useSignUpForm() {
  const form = useForm();
  const stage = useStage();
  const status = useFormState((state) => state.status);
  const page = usePageList().at(-1)?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (status !== 'ready' || !page || added.current) return;
    added.current = true;
    const at = (y: number, width = 240, height = 24) => ({ page, rect: { x: 72, y, width, height } });
    void (async () => {
      await form.create({ family: 'text', name: 'name', widgets: [{ ...at(540), ...look }] });
      await form.create({ family: 'text', name: 'email', widgets: [{ ...at(576), ...look }] });
      await form.create({
        family: 'combobox',
        name: 'framework',
        options: ['React', 'Vue', 'Svelte', 'Angular'].map((label) => ({ label, value: label })),
        widgets: [{ ...at(612, 160), ...look }],
      });
      await form.create({ family: 'checkbox', name: 'updates', widgets: [{ ...at(650, 16, 16), ...look }] });
      stage.goToPage(page);
    })();
  }, [form, stage, status, page]);
}

function FormToolbar() {
  useSignUpForm();
  const { fields } = useFormState();

  return (
    <div className="toolbar">
      <output className="readout">
        {fields.length === 0 ? 'Adding a form…' : `${fields.length} fields: click one and fill it in`}
      </output>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <FormToolbar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <FormLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`basic.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --canvas: #f7f9fc;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

/* The pages */
.stage {
  height: 440px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

Each field becomes a real HTML control, placed and styled like the field in the PDF: people type in text
boxes, tick checkboxes, pick from lists, and move between fields with Tab. A value is saved into the
document as it's entered, so it's there when the PDF is downloaded, and every PDF app shows it. The
form layer paints the fields as the engine draws them, so while it's on the page, the render layer
leaves them out of the page picture.

## Read the form

`useFormState()` gives you every field, and re-renders when a value changes:

**`field-list.tsx`**

```tsx
import { useFormState } from '@embedpdf/react/form';

export function FieldList() {
  const { fields } = useFormState();

  return (
    <ul>
      {fields.map((field) => (
        <li key={field.name}>
          {field.name}: {field.family}
        </li>
      ))}
    </ul>
  );
}
```

Each field has its full `name` (such as `billing.name`), its `family` (`'text'`, `'checkbox'`,
`'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'` or `'signature'`), its value, and a `ref`: its
address for everything else. The fields and their values are the engine's; see
[Forms](https://www.embedpdf.com/docs/engine/forms#read-the-form) for every family's value.

For one field, such as a total you show in your own UI, use `useFormValue()`:

**`invoice-total.tsx`**

```tsx
import { toFieldRef, useFormValue } from '@embedpdf/react/form';

export function InvoiceTotal() {
  const total = useFormValue(toFieldRef('invoice.total'));

  return <output>{total && 'value' in total && total.value}</output>;
}
```

`toFieldRef(name)` makes a ref from a field's full name, for when you know a field by name.

**`read.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  FormLayer,
  formPlugin,
  toFieldRef,
  useForm,
  useFormEvent,
  useFormState,
  useFormValue,
} from '@embedpdf/react/form';
import type { FormFieldDTO } from '@embedpdf/react/form';
import { localEngine } from '@embedpdf/engine';

import './read.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// How the new fields look: part of the PDF, like the rest of the page.
const look = { color: '#94a3b8', interiorColor: '#f8fafc', strokeWidth: 1, fontSize: 11 };

/** The ebook has no form, so this adds one to its last page, fills in a name, and goes there. */
function useSignUpForm() {
  const form = useForm();
  const stage = useStage();
  const status = useFormState((state) => state.status);
  const page = usePageList().at(-1)?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (status !== 'ready' || !page || added.current) return;
    added.current = true;
    const at = (y: number, width = 240, height = 24) => ({ page, rect: { x: 72, y, width, height } });
    void (async () => {
      await form.create({ family: 'text', name: 'name', widgets: [{ ...at(540), ...look }] });
      await form.create({ family: 'text', name: 'email', widgets: [{ ...at(576), ...look }] });
      await form.create({
        family: 'combobox',
        name: 'framework',
        options: ['React', 'Vue', 'Svelte', 'Angular'].map((label) => ({ label, value: label })),
        widgets: [{ ...at(612, 160), ...look }],
      });
      await form.create({ family: 'checkbox', name: 'updates', widgets: [{ ...at(650, 16, 16), ...look }] });
      await form.setValue(toFieldRef('name'), { value: 'Ada Lovelace' });
      stage.goToPage(page);
    })();
  }, [form, stage, status, page]);
}

/** A field's value as text, whatever its family. */
function valueText(field: FormFieldDTO): string {
  switch (field.family) {
    case 'text':
    case 'combobox':
      return field.value || '—';
    case 'checkbox':
      return field.checked ? 'checked' : 'not checked';
    case 'radio':
      return field.value === 'Off' ? '—' : field.value;
    case 'listbox':
      return field.selectedValues.join(', ') || '—';
    default:
      return '';
  }
}

function Greeting() {
  useSignUpForm();
  const name = useFormValue(toFieldRef('name'));
  const [changed, setChanged] = useState<string | null>(null);

  // Every change, whoever made it: typing, a script, or code.
  useFormEvent(
    (form) => form.onValueChanged,
    ({ field }) => setChanged(field.name),
  );

  return (
    <div className="toolbar">
      <output className="readout">
        Hello, {name && 'value' in name && name.value ? name.value : 'stranger'}
      </output>
      {changed && <output className="note">Last change: {changed}</output>}
    </div>
  );
}

function FieldList() {
  const { fields } = useFormState();

  return (
    <ul className="fields">
      {fields.map((field) => (
        <li key={field.name} className="field">
          <span className="field-name">
            {field.name} <span className="family">{field.family}</span>
          </span>
          <span className="field-value">{valueText(field)}</span>
        </li>
      ))}
    </ul>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <Greeting />
        <div className="viewer">
          <Stage className="stage">
            {() => (
              <>
                <RenderLayer />
                <FormLayer />
              </>
            )}
          </Stage>
          <FieldList />
        </div>
      </DocumentGate>
    </Viewer>
  );
}
```

**`read.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --text: #3b4b7a;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The greeting, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 12px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.note {
  font-size: 12px;
  color: var(--muted);
}

/* The pages and the fields: stacked, and side by side where there's room */
.viewer {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.stage,
.fields {
  flex: 1 1 100%;
  box-sizing: border-box;
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.stage {
  height: 340px;
  background: var(--canvas);
}

.fields {
  margin: 0;
  padding: 6px;
  list-style: none;
  background: var(--surface);
}

@container (min-width: 620px) {
  .stage {
    flex: 1 1 0;
    min-width: 0;
    height: 440px;
  }

  .fields {
    flex: 0 0 220px;
    max-height: 440px;
    overflow-y: auto;
  }
}

/* One field: its name and family, then its value */
.field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 8px 10px;
  border-radius: 9px;
}

.field + .field {
  border-top: 1px solid var(--line);
}

.field-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.family {
  font-size: 11px;
  font-weight: 500;
  color: var(--muted);
}

.field-value {
  font-size: 13px;
  color: var(--text);
  overflow-wrap: anywhere;
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Fill in a field

`setValue()` takes the same values a field reads:

```ts
await form.setValue(name.ref, { value: 'Ada Lovelace' }); // text
await form.setValue(agree.ref, { checked: true }); // checkbox
await form.setValue(plan.ref, { value: 'yearly' }); // radio group: the button's export value
await form.setValue(country.ref, { value: 'NL' }); // dropdown: an option's value
await form.setValue(toppings.ref, { selectedValues: ['cheese', 'olives'] }); // list
```

It runs the same checks and scripts as typing does, and resolves with `{ field, status }`: `status`
is `'applied'`, `'unchanged'`, or `'rejected'` when the form's own validation refused the value.

The field shows the new value at once, before the engine answers, and the promise resolves once it
has. What the form's scripts calculate from it goes with it, as one change: one request, and one
step to undo. The field stays editable meanwhile; filling it in again is the next change. A value
the engine refuses goes back to the one the field had.

To fill many fields at once, such as from your backend, pass them by name:

```ts
await form.importValues({ 'billing.name': 'Ada Lovelace', 'billing.country': 'NL', agree: true });
```

`importValues()` and `setValues()` write every value as one change, with the calculations run once
after them. Each field's own keystroke and validation scripts don't run, as when a script sets a
field's `value` in Acrobat.

**`fill.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { FormLayer, formPlugin, toFieldRef, useForm, useFormState } from '@embedpdf/react/form';
import type { FormCapability } from '@embedpdf/react/form';
import { localEngine } from '@embedpdf/engine';

import './fill.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// How the new fields look: part of the PDF, like the rest of the page.
const look = { color: '#94a3b8', interiorColor: '#f8fafc', strokeWidth: 1, fontSize: 11 };

/** One field of each family, with `setValue()` in the shape each one takes. */
async function fillIn(form: FormCapability) {
  await form.setValue(toFieldRef('name'), { value: 'Ada Lovelace' }); // text
  await form.setValue(toFieldRef('updates'), { checked: true }); // checkbox
  await form.setValue(toFieldRef('plan'), { value: 'yearly' }); // radio group: a button's value
  await form.setValue(toFieldRef('framework'), { value: 'React' }); // dropdown: an option's value
  return form.setValue(toFieldRef('topics'), { selectedValues: ['Forms', 'Signatures'] }); // list
}

/** The ebook has no form, so this adds one to its last page, fills it in, and goes there. */
function useSignUpForm() {
  const form = useForm();
  const stage = useStage();
  const status = useFormState((state) => state.status);
  const page = usePageList().at(-1)?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (status !== 'ready' || !page || added.current) return;
    added.current = true;
    const at = (x: number, y: number, width: number, height: number) => ({
      page,
      rect: { x, y, width, height },
      ...look,
    });
    void (async () => {
      await form.create({ family: 'text', name: 'name', widgets: [at(72, 520, 220, 22)] });
      await form.create({
        family: 'combobox',
        name: 'framework',
        options: ['React', 'Vue', 'Svelte', 'Angular'].map((label) => ({ label, value: label })),
        widgets: [at(72, 552, 160, 22)],
      });
      await form.create({
        family: 'radio',
        name: 'plan',
        widgets: [
          { ...at(72, 588, 16, 16), exportValue: 'monthly' },
          { ...at(112, 588, 16, 16), exportValue: 'yearly' },
        ],
      });
      await form.create({ family: 'checkbox', name: 'updates', widgets: [at(72, 618, 16, 16)] });
      await form.create({
        family: 'listbox',
        name: 'topics',
        multiSelect: true,
        options: ['Forms', 'Annotations', 'Signatures'].map((label) => ({ label, value: label })),
        widgets: [at(320, 520, 150, 60)],
      });
      await fillIn(form);
      stage.goToPage(page);
    })();
  }, [form, stage, status, page]);
}

function FillToolbar() {
  useSignUpForm();
  const form = useForm();
  const [result, setResult] = useState('');

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        onClick={async () => {
          const { status } = await fillIn(form);
          setResult(`Filled in: ${status}`);
        }}
      >
        Fill in
      </button>
      <button
        type="button"
        className="button"
        onClick={async () => {
          // Plain values by full name, as your backend would send them.
          const { applied, skipped } = await form.importValues({
            name: 'Grace Hopper',
            framework: 'Vue',
            updates: false,
            topics: ['Annotations'],
            phone: '555-0100',
          });
          setResult(`${applied.length} filled, ${skipped.length} skipped`);
        }}
      >
        Fill from your backend
      </button>
      <button
        type="button"
        className="button"
        onClick={async () => {
          const { fields } = await form.reset();
          setResult(`${fields.length} fields reset`);
        }}
      >
        Reset
      </button>
      {result && <output className="readout">{result}</output>}
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <FillToolbar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <FormLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`fill.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 13px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

/* The pages */
.stage {
  height: 440px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Check required fields

A form marks some fields as required. Check them before you submit or download:

```ts
const { valid, missing } = form.validate();

const widget = !valid && form.getWidget(missing[0].widgets[0]);
if (widget) stage.reveal(widget.page, { rect: widget.rect });
```

`missing` lists the required fields that are empty, in page order, so you can take the user to the
first one. Only the fields the user may fill in count: another signer's are theirs to fill.

**`validate.tsx`**

```tsx
import { useCallback, useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { FormLayer, formPlugin, toFieldRef, useForm, useFormState } from '@embedpdf/react/form';
import type { FormValidation } from '@embedpdf/react/form';
import { localEngine } from '@embedpdf/engine';

import './validate.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// How the new fields look: part of the PDF, like the rest of the page.
const look = { color: '#94a3b8', interiorColor: '#f8fafc', strokeWidth: 1, fontSize: 11 };

/** The ebook has no form, so this adds one to its last page, with three required fields. */
function useSignUpForm(onReady: () => void) {
  const form = useForm();
  const status = useFormState((state) => state.status);
  const page = usePageList().at(-1)?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (status !== 'ready' || !page || added.current) return;
    added.current = true;
    const at = (y: number, width = 240, height = 24) => ({ page, rect: { x: 72, y, width, height } });
    void (async () => {
      await form.create({
        family: 'text',
        name: 'name',
        required: true,
        widgets: [{ ...at(540), ...look }],
      });
      await form.create({
        family: 'text',
        name: 'email',
        required: true,
        widgets: [{ ...at(576), ...look }],
      });
      await form.create({ family: 'text', name: 'company', widgets: [{ ...at(612), ...look }] });
      await form.create({
        family: 'checkbox',
        name: 'terms',
        required: true,
        widgets: [{ ...at(650, 16, 16), ...look }],
      });
      await form.setValue(toFieldRef('name'), { value: 'Ada Lovelace' });
      onReady();
    })();
  }, [form, status, page, onReady]);
}

function SubmitToolbar() {
  const form = useForm();
  const stage = useStage();
  const [check, setCheck] = useState<FormValidation | null>(null);

  // Check the required fields, and take the reader to the first empty one.
  const submit = useCallback(() => {
    const result = form.validate();
    setCheck(result);
    // Where a field shows is its widget's row in the form.
    const first = result.missing[0]?.widgets[0];
    const widget = first ? form.getWidget(first) : null;
    if (widget) stage.reveal(widget.page, { rect: widget.rect });
  }, [form, stage]);
  useSignUpForm(submit);

  return (
    <div className="toolbar">
      <button type="button" className="button" onClick={submit}>
        Submit
      </button>
      {check &&
        (check.valid ? (
          <output className="readout">Every required field is filled in</output>
        ) : (
          <output className="readout missing">
            Fill in first: {check.missing.map((field) => field.name).join(', ')}
          </output>
        ))}
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <SubmitToolbar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <FormLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`validate.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --warn: #b45309;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 13px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.missing {
  color: var(--warn);
}

/* The pages */
.stage {
  height: 440px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Calculations, formatting and validation

Many forms add up totals, format dates and check values with scripts. With JavaScript on in the
[actions plugin](documents/actions), they run as people type, as in Acrobat:

**`scripts.ts`**

```typescript
import { actionsPlugin } from '@embedpdf/react/actions';
import { formPlugin } from '@embedpdf/react/form';

export const plugins = [/* … */ formPlugin(), actionsPlugin({ javascript: { enabled: true } })];
```

When a script refuses a value, such as a date in the wrong format, the field keeps its old value,
the box keeps what was typed so it can be corrected, as in Acrobat, and
`onValidationRejected` tells you why, so you can show a message.

The scripts read the form as the user sees it, values still on their way included, so nothing is
read from the engine for them. Their engine starts when the first field of a form with scripts takes
focus, so the first value doesn't wait for it; a form without scripts never starts it.

## Reset the form

```ts
await form.reset(); // every field the user may fill in, back to its default
await form.reset([name.ref, email.ref]); // some fields
```

## Move form data in and out

For your own backend, plain values are simplest:

```ts
const values = form.exportValues(); // { 'billing.name': 'Ada Lovelace', agree: true, … }
await form.importValues(values);
```

Fields are matched by their full name. A field the form doesn't have, or a value it can't take, is
skipped; the rest still land.

To copy the fields themselves to another document, with their boxes, values and scripts, take
them out as a bundle and import it there. `FormTransfer` turns a bundle into one JSON text to
store, and back:

```ts
const bundle = await form.export(); // every field; or form.export({ fields }), ({ pages })
const text = FormTransfer.stringify(bundle);

const { fields, dropped } = await otherForm.import(await FormTransfer.parse(text));
```

The import adds the fields as one change you can undo. A field whose name the form has already is
left out and listed in `dropped`. By default each field keeps who created and filled it in, which
needs `canRestoreAttribution()`; pass `{ attribution: 'stamp' }` to make them yours. See
[Import & export](https://www.embedpdf.com/docs/engine/forms/import-export).

**`data.tsx`**

```tsx
import { useEffect, useRef, useState } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  FormLayer,
  FormTransfer,
  formPlugin,
  useForm,
  useFormState,
  type FormBundle,
} from '@embedpdf/react/form';
import { localEngine } from '@embedpdf/engine';

import './data.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), formPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// How the new fields look: part of the PDF, like the rest of the page.
const look = { color: '#94a3b8', interiorColor: '#f8fafc', strokeWidth: 1, fontSize: 11 };

/** The ebook has no form, so this adds one to its last page, fills it in, and goes there. */
function useSignUpForm() {
  const form = useForm();
  const stage = useStage();
  const status = useFormState((state) => state.status);
  const page = usePageList().at(-1)?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (status !== 'ready' || !page || added.current) return;
    added.current = true;
    const at = (y: number, width = 240, height = 24) => ({ page, rect: { x: 72, y, width, height } });
    void (async () => {
      await form.create({ family: 'text', name: 'name', widgets: [{ ...at(540), ...look }] });
      await form.create({ family: 'text', name: 'email', widgets: [{ ...at(576), ...look }] });
      await form.create({
        family: 'combobox',
        name: 'framework',
        options: ['React', 'Vue', 'Svelte', 'Angular'].map((label) => ({ label, value: label })),
        widgets: [{ ...at(612, 160), ...look }],
      });
      await form.create({ family: 'checkbox', name: 'updates', widgets: [{ ...at(650, 16, 16), ...look }] });
      await form.importValues({ name: 'Ada Lovelace', email: 'ada@example.com', framework: 'Svelte' });
      stage.goToPage(page);
    })();
  }, [form, stage, status, page]);
}

function DataToolbar() {
  useSignUpForm();
  const form = useForm();
  const [bundle, setBundle] = useState<FormBundle | null>(null);
  const [result, setResult] = useState('');

  return (
    <div className="toolbar">
      <button
        type="button"
        className="button"
        onClick={async () => {
          const exported = await form.export();
          // One JSON text: what you'd store, and read back with FormTransfer.parse().
          const text = FormTransfer.stringify(exported);
          setBundle(exported);
          setResult(`Exported ${exported.fields.length} fields, ${text.length} characters of JSON`);
        }}
      >
        Export
      </button>
      <button
        type="button"
        className="button"
        onClick={async () => {
          for (const field of form.list()) await form.delete(field.ref);
          setResult('The form is gone');
        }}
      >
        Remove
      </button>
      <button
        type="button"
        className="button"
        disabled={!bundle}
        onClick={async () => {
          if (!bundle) return;
          const { fields, dropped } = await form.import(bundle);
          setResult(`Imported ${fields.length} fields, left out ${dropped.length}`);
        }}
      >
        Import
      </button>
      {result && <output className="readout">{result}</output>}
    </div>
  );
}

/** The values as plain data, keyed by full name: what you'd send to your backend. */
function Values() {
  const form = useForm();
  // The state changes with every value, so this reads them again each time.
  useFormState((state) => state.fields);

  return <pre className="values">{JSON.stringify(form.exportValues(), null, 2)}</pre>;
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <DataToolbar />
        <div className="viewer">
          <Stage className="stage">
            {() => (
              <>
                <RenderLayer />
                <FormLayer />
              </>
            )}
          </Stage>
          <Values />
        </div>
      </DocumentGate>
    </Viewer>
  );
}
```

**`data.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --text: #3b4b7a;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 13px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

/* The pages and the values: stacked, and side by side where there's room */
.viewer {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.stage,
.values {
  flex: 1 1 100%;
  box-sizing: border-box;
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.stage {
  height: 340px;
  background: var(--canvas);
}

.values {
  margin: 0;
  padding: 12px 14px;
  overflow: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text);
  background: var(--surface);
}

@container (min-width: 620px) {
  .stage {
    flex: 1 1 0;
    min-width: 0;
    height: 440px;
  }

  .values {
    flex: 0 0 230px;
    max-height: 440px;
  }
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Methods

What `useForm()` gives you for reading and filling. The verbs that change the form itself are
on [Building forms](forms/building#methods).

| Method                                                        | What it does                                                                                                                                                                                            |
| :------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `get(ref)`                                                    | One field, with its value, or `null` when the form has no such field. [Read the form](#read-the-form)                                                                                                   |
| `getValue(ref)`                                               | One field's value, in the shape `setValue()` takes, or `null`. In a component, use `useFormValue(ref)`.                                                                                                 |
| `getFieldForWidget(widget)`                                   | The field a box belongs to, from the box's ref                                                                                                                                                          |
| `getWidget(widget)`                                           | A box's row: its page, its place (`rect`), its look and its state, or `null` when no page shows it                                                                                                      |
| `getWidgets(ref)`                                             | Every box a field shows in, as rows in the field's order: a radio group's buttons, or a field repeated on several pages                                                                                 |
| `listWidgets(page)`                                           | Each field box on a page, with its place, its control, its value and whether it can be filled. For drawing the fields yourself.                                                                         |
| `getWidgetAt(page, point)`                                    | The field box under a point on a page, with its field, or `null`                                                                                                                                        |
| `setValue(ref, value)`                                        | Fills in a field with `{ value }`, `{ checked }` or `{ selectedValues }`, running the form's checks and scripts. Resolves `{ field, status }`. [Fill in a field](#fill-in-a-field)                      |
| `setValues(entries)`                                          | Fills in several fields, each `{ ref, value }`, as one change with one recalculation; a field you may not fill in, or a value it can't take, fails on its own. Resolves `{ applied, skipped, failed }`. |
| `importValues(values)`                                        | Fills in fields by full name from a plain object, as one change with one recalculation. Resolves `{ applied, skipped, failed }`. [Move form data in and out](#move-form-data-in-and-out)                |
| `exportValues()`                                              | Every value, keyed by full name, as a plain object                                                                                                                                                      |
| `export(selection?)`                                          | Whole fields with their boxes, values and scripts, as a bundle: every field, or `{ fields }` or `{ pages }`. [Move form data in and out](#move-form-data-in-and-out)                                    |
| `import(bundle, options?)`                                    | Adds a bundle's fields, as one change. Resolves `{ fields, refMap, dropped }`. Needs `canDesign()`. [Move form data in and out](#move-form-data-in-and-out)                                             |
| `reset(refs?)`                                                | Puts back each field's default, for every field the user may fill in or the fields you pass. Resolves `{ fields }`, the fields it changed. [Reset the form](#reset-the-form)                            |
| `validate()`                                                  | Checks the required fields the user may fill in. Returns `{ valid, missing }`, the empty ones in page order. [Check required fields](#check-required-fields)                                            |
| `activateWidget(widget)`                                      | Does what a click on a field box does, such as running a button's action                                                                                                                                |
| `getSnapshot()`                                               | Everything at once, as the user sees it: what the engine's `list()` returns, with the values still on their way. `fields`, `widgets`, `formKind`, `needsAppearances` and `calculationOrder`             |
| `refresh()`                                                   | Reads the whole form again, and `onResynced` fires. You rarely need it: the plugin follows every change, whoever made it.                                                                               |
| `canRead()`, `canFill(ref)`                                   | Whether reading the form, and filling in a field, are allowed. [Permissions](#permissions)                                                                                                              |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useFormSettings()`. [Settings](#settings)                                                |

Every method that returns a promise also takes a `signal` in its options, to cancel it.

## State

Everything `useFormState()` gives you. Your component re-renders when a value changes. The method in the last column reads the same value once.

| Field           | What it is                                                                                                              | Read once                                            |
| :-------------- | :---------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------- |
| `fields`        | Every field, with its value                                                                                             | `list()`, or `list({ family, page, name })` for some |
| `status`        | Whether the form is read: `'idle'`, `'loading'`, `'ready'`, `'forbidden'` or `'error'`                                  | `getStatus()`                                        |
| `formKind`      | What the document has: `'none'`, `'acroform'` (a regular PDF form) or `'xfa'`                                           | `getFormKind()`                                      |
| `selectedField` | In design mode, the field of the selected box, or `null`; see [Building forms](forms/building#change-a-fields-settings) | `getSelectedField()`                                 |

For one field, `useFormValue(ref)` re-renders only when that field's value changes.

## Settings

Set them when you register the plugin, and change them while the app runs with
`form.updateSettings({ … })`; `form.resetSettings()` goes back to what you registered, and
`useFormSettings()` gives you the current settings.

| Setting             | What it does                                                              | Default                     | From CSS                       |
| :------------------ | :------------------------------------------------------------------------ | :-------------------------- | :----------------------------- |
| `validation`        | Run the form's own checks (`'scripts'`), or accept every value (`'none'`) | `'scripts'`                 | —                              |
| `focus.color`       | The ring around the field being filled in                                 | the viewer's accent         | `--epdf-form-focus`            |
| `fields.border`     | The edge of a field that has no border of its own                         | the viewer's accent at 55%  | `--epdf-form-field-border`     |
| `fields.background` | Inside a field that has no background of its own                          | `'rgb(255 255 255 / 0.92)'` | `--epdf-form-field-background` |
| `fields.color`      | The check mark and text of a field that has no color of its own           | `'#1f2a44'`                 | `--epdf-form-field-color`      |
| `fillable`          | Which fields people fill in by hand: `true`, `false`, or field families   | `true`                      | —                              |

Scripts need JavaScript on in the actions plugin; without it, `'scripts'` accepts every value too,
and a form with scripts logs a hint once.

`fillable` is for the people using your app. A field it leaves out draws as a read-only field
does, and takes no typing, clicks or picks; your code still fills it in with `setValue()` and
`importValues()`, and `canFill(ref)` doesn't change:

```ts
formPlugin({ fillable: false }); // show the values, take no input
form.updateSettings({ fillable: ['text', 'checkbox'] }); // only these families
form.updateSettings({ fillable: true }); // every field again
```

The families are `'text'`, `'checkbox'`, `'radio'`, `'combobox'`, `'listbox'`, `'pushbutton'` and
`'signature'` (signing a field is filling it in). To stop your code too, leave `doc.forms.fill` out
of the scope (see [Permissions](#permissions)).

The colors are what the viewer draws around fields; a field's own colors are saved in the PDF (see
[Building forms](forms/building)). A CSS variable wins over the setting: see
[Colors & theming](ui/theming).

**`colors.tsx`**

```tsx
import { useEffect, useRef } from 'react';
import { Viewer, DocumentGate, usePageList } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import {
  FormLayer,
  formPlugin,
  useForm,
  useFormSettings,
  useFormState,
} from '@embedpdf/react/form';
import { localEngine } from '@embedpdf/engine';

import './colors.css';

const engine = localEngine();
// The colors the viewer draws around fields; `null` follows the viewer's accent.
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  formPlugin({ fields: { border: '#ea580c' } }),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const COLORS = [
  ['Accent', null],
  ['Orange', '#ea580c'],
  ['Green', '#16a34a'],
] as const;

/** The ebook has no form, so this adds one to its last page: fields with no border of their own. */
function useSignUpForm() {
  const form = useForm();
  const stage = useStage();
  const status = useFormState((state) => state.status);
  const page = usePageList().at(-1)?.ref;
  const added = useRef(false);

  useEffect(() => {
    if (status !== 'ready' || !page || added.current) return;
    added.current = true;
    const at = (y: number, width = 240, height = 24) => ({ page, rect: { x: 72, y, width, height } });
    void (async () => {
      await form.create({ family: 'text', name: 'name', widgets: [{ ...at(540), fontSize: 11 }] });
      await form.create({ family: 'text', name: 'email', widgets: [{ ...at(576), fontSize: 11 }] });
      await form.create({ family: 'checkbox', name: 'updates', widgets: [at(614, 16, 16)] });
      stage.goToPage(page);
    })();
  }, [form, stage, status, page]);
}

function ColorToolbar() {
  useSignUpForm();
  const form = useForm();
  const settings = useFormSettings();

  return (
    <div className="toolbar">
      <span className="label">Edges</span>
      <div className="segments">
        {COLORS.map(([label, color]) => (
          <button
            key={label}
            type="button"
            className="segment"
            aria-pressed={settings.fields.border === color}
            onClick={() => form.updateSettings({ fields: { border: color } })}
          >
            {label}
          </button>
        ))}
      </div>
      <span className="label">Focus</span>
      <div className="segments">
        {COLORS.map(([label, color]) => (
          <button
            key={label}
            type="button"
            className="segment"
            aria-pressed={settings.focus.color === color}
            onClick={() => form.updateSettings({ focus: { color } })}
          >
            {label}
          </button>
        ))}
      </div>
    </div>
  );
}

export default function App() {
  return (
    <Viewer engine={engine} plugins={plugins} initialDocuments={[{ source: ebook }]}>
      <DocumentGate fallback={<p className="loading">Loading…</p>}>
        <ColorToolbar />
        <Stage className="stage">
          {() => (
            <>
              <RenderLayer />
              <FormLayer />
            </>
          )}
        </Stage>
      </DocumentGate>
    </Viewer>
  );
}
```

**`colors.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The color choices, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  margin-bottom: 14px;
}

.label {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

.segments {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 3px;
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.segment {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 7px;
  padding: 7px 12px;
  cursor: pointer;
}

.segment:hover {
  color: var(--ink);
}

.segment[aria-pressed='true'] {
  color: var(--accent);
  background: var(--surface);
  box-shadow: 0 1px 3px rgb(7 32 76 / 0.12);
}

.segment:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

/* The pages */
.stage {
  height: 440px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Events

**`autosave.tsx`**

```tsx
import { useFormEvent } from '@embedpdf/react/form';
import { autosave } from './api';

export function Autosave() {
  useFormEvent(
    (form) => form.onValueChanged,
    ({ field }) => autosave(field.name),
  );

  return null;
}
```

| Event                  | Fires when                                                               | Carries               |
| :--------------------- | :----------------------------------------------------------------------- | :-------------------- |
| `onValueChanged`       | A field's value changed                                                  | `field`, `origin`     |
| `onValidationRejected` | The form's validation refused a value                                    | `field`, `issues`     |
| `onResynced`           | The whole form was read, as when it opens, after `refresh()` or a repair | `snapshot`            |
| `onSettingsChanged`    | The settings changed                                                     | `settings`, `changed` |

Fields that are added, changed or removed have their own events; see
[Building forms](forms/building#events).

## Permissions

| Permission       | What it allows   | Check               |
| :--------------- | :--------------- | :------------------ |
| `doc.forms.read` | Reading the form | `form.canRead()`    |
| `doc.forms.fill` | Filling it in    | `form.canFill(ref)` |

Without it, the fields show their values but can't be changed. A user with a `fields:fill`
permission for one group fills in that group's fields only: `form.canFill(ref)` answers per field,
the other fields can't be changed and Tab skips them, `validate()` leaves them out, and `reset()`
leaves them as they are. See [Signers and groups](https://www.embedpdf.com/docs/engine/forms/groups).

## Good to know

### Forms and annotations

A field's widgets are the form's: the annotation plugin never lists them. With the
[annotation plugin](annotations) registered, the `<FormLayer>` and the annotation layer share the page: fields stay fillable with the
`pointer` tool, and become movable boxes in [design mode](forms/building).

### Keyboard

Tab moves to the next field in the page's order. In a text box, Enter confirms and Escape puts back
the value it had.

### Collecting responses

On the local engine, the values live in the document in this tab until it's downloaded.

> **Collect form responses on your server**
>
> With CloudPDF, each value is stored on your server as it's entered, so a half-filled form survives a
> closed tab, and your backend reads the answers without asking the user to download or upload a
> file.
>
> [Collect forms with CloudPDF](https://www.cloudpdf.com/docs/headless/react/forms)

### XFA forms

Some PDFs also carry a form in the older XFA format. The viewer works with the document's regular
fields and never runs XFA; see [Forms](https://www.embedpdf.com/docs/engine/forms#xfa-forms).

### Signed documents

A signature can lock the whole form, or some fields. Locked fields show their values and can't be
changed; see [Signatures](forms/signatures).
