Guide

Form-Edit Mode

Place, move, resize, and configure form fields directly on the document

Form-edit mode turns the viewer into a form designer. Users can drop new form fields onto pages, drag and resize existing ones, edit field properties (name, default value, options, lock rules, …), and tag fields with roles for color-coded multi-signer flows.

When form-edit mode is off, fields render normally — fillable but not movable. When it's on, every field gets selection chrome and a property sidebar appears.

Toggling the Mode

The default ViewerBar includes a form-edit toggle button (i-lucide-form-input). For a programmatic toggle, use the formEditMode prop or the exposed methods.

Via v-model

<template>
  <KViewer
    v-model:form-edit-mode="isEditing"
    :source="pdfUrl"
  />
  <button @click="isEditing = !isEditing">
    {{ isEditing ? 'Done' : 'Edit fields' }}
  </button>
</template>

<script setup lang="ts">
const isEditing = ref(false)
</script>

Via Methods

const viewer = ref()

viewer.value?.setFormEditMode(true)         // enter
viewer.value?.toggleFormEditMode()          // flip, returns the new state
const editing = viewer.value?.formEditMode  // reactive Ref<boolean>
formEditMode exposed on the viewer is a reactive Ref<boolean> — read it with .value.

Roles

Real-world form workflows usually have multiple parties touching the same document — a signer, a witness, an approver, a "fill out section A" role, etc. Form-edit mode supports this with an opaque role-tag system: each field carries an optional roleId, and the viewer paints chrome for that field in the role's color.

The viewer stays unopinionated about what a role is. The host app:

  1. Owns the role list (names, ids, palette, persistence).
  2. Hands the viewer a flat roleId → CSS color map via the roleColors prop.
  3. Drives which role new placements adopt via v-model:active-role-id.

That's the whole API surface. Call them "signers", "approvers", "departments" — the viewer doesn't care.

Defining Roles

<template>
  <!-- Your role picker UI -->
  <div class="flex gap-2">
    <button
      v-for="r in roles"
      :key="r.id"
      :style="{ borderColor: r.color }"
      :class="{ 'ring-2': activeRoleId === r.id }"
      @click="activeRoleId = activeRoleId === r.id ? null : r.id"
    >
      {{ r.name }}
    </button>
  </div>

  <KViewer
    v-model:active-role-id="activeRoleId"
    :source="pdfUrl"
    :role-colors="roleColors"
    form-edit-mode
  />
</template>

<script setup lang="ts">
const roles = ref([
  { id: 'signer',   name: 'Signer',   color: '#1677ff' },
  { id: 'approver', name: 'Approver', color: '#52c41a' },
  { id: 'witness',  name: 'Witness',  color: '#fa8c16' },
])

const activeRoleId = ref<string | null>(null)

// Flat map fed to the viewer. Recomputes when colors change so chrome
// repaints immediately.
const roleColors = computed<Record<string, string>>(() => {
  const out: Record<string, string> = {}
  for (const r of roles.value) out[r.id] = r.color
  return out
})
</script>

When activeRoleId is set, fields placed via the placement tool — or via addFormField() without an explicit roleId — inherit the active role automatically. Fields tagged with an unknown or removed role id fall back to the default blue.

Role tags are a viewer-only color hint. They don't round-trip into the exported PDF — they're meant for the editing UI, not the document.

Placing Fields in the UI

In form-edit mode the toolbar swaps drawing tools for field-placement tools (text, checkbox, radio, dropdown, signature). Pick a tool, drag a rect on the page, and the field is placed.

Radio grouping has a quality-of-life shortcut: if you have a radio currently selected when you place a new radio, the new widget joins that radio's group automatically. To start a fresh group, deselect first or change the group via the property sidebar.

For programmatic placement, see Add Fields Programmatically.

Placing Fields Without Form-Edit Mode

Form-edit mode is the full editing experience: swapped toolbar, chrome on every field, property sidebar. When you only need "let the user drop a signature field", that is too much UI — e.g. an e-sign flow where a seller marks where customers sign, and everything else is configured by the host app.

For that, use the mode-independent placement tool IDs (placeText, placeCheckbox, placeRadio, placeSignature) in the tools prop. They render next to the draw tools, work exactly like the rectangle tool (drag a rect, or click for a default-sized field), and do not switch the viewer into form-edit mode.

Three companion features complete the flow:

  • editablePlacedFields — placed fields keep selection chrome in hand mode, so users can move/resize/delete them without form-edit mode. Fields parsed from the source PDF stay untouched.
  • fieldDefaults — static per-type presets applied to every placement.
  • The field-placed event — fires with the created FormFieldDefinition; patch it via updateFormField() for per-field properties like a unique name.
<template>
  <KViewer
    ref="viewer"
    :source="pdfUrl"
    :tools="tools"
    editable-placed-fields
    :field-defaults="{ signature: { promptText: 'Sign here', lockAction: 'all' } }"
    @field-placed="onFieldPlaced"
  />
</template>

<script setup lang="ts">
import type { FormFieldDefinition, ViewerToolEntry } from 'kviewer'

const viewer = ref()

const tools: ViewerToolEntry[] = [
  // top row
  'menu', 'pageSettings', 'separator', 'zoom', 'hand', 'marquee',
  'spacer', 'pageInfo', 'search',
  // second row: draw tools plus the signature-field placement button
  'properties', 'spacer',
  'freehand', 'freeHighlight', 'freeText', 'stamp', 'rectangle',
  'separator', 'placeSignature',
  'spacer', 'undo', 'redo', 'eraser',
]

let fieldCount = 0
function onFieldPlaced(field: FormFieldDefinition) {
  fieldCount += 1
  viewer.value?.updateFormField(field.id, {
    fieldName: `customer_signature_${fieldCount}`,
  })
}
</script>
Signature fields placed this way render as passive markers unless clickToSign is enabled, so users can't accidentally open kviewer's own signing canvas while the host owns the signing process.

Common Patterns

Open the Document Already in Edit Mode

<KViewer :source="pdfUrl" form-edit-mode />

Toggle Edit Mode From a Custom Header

<template>
  <KViewer
    ref="viewer"
    v-model:form-edit-mode="editing"
    :source="pdfUrl"
  >
    <template #header>
      <div class="flex items-center justify-between p-2">
        <span class="font-medium">My Custom Toolbar</span>
        <UButton
          :icon="editing ? 'i-lucide-check' : 'i-lucide-pencil'"
          :label="editing ? 'Done' : 'Edit form'"
          @click="viewer?.toggleFormEditMode()"
        />
      </div>
    </template>
  </KViewer>
</template>

<script setup lang="ts">
const viewer = ref()
const editing = ref(false)
</script>

Pre-Configure a Form Programmatically

Combine setFormEditMode with addFormField to seed a document with a known field layout:

const viewer = ref()

async function setupContract() {
  viewer.value?.setFormEditMode(true)

  viewer.value?.addFormField({
    pageNumber: 1,
    fieldType: 'text',
    rect: [50, 700, 250, 720],
    fieldName: 'fullName',
    roleId: 'signer',
    required: true,
  })

  viewer.value?.addFormField({
    pageNumber: 1,
    fieldType: 'signature',
    rect: [50, 640, 250, 680],
    fieldName: 'signature',
    roleId: 'signer',
    promptText: 'Click to sign',
    lockAction: 'all',
  })
}
Copyright © 2026