Form-Edit Mode
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 (host-side)
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. The viewer has no role API for this on purpose: a "role" is just a bundle of per-field properties the host applies via fieldDefaults — a fieldColor for the chrome and a meta tag for identification.
The host app:
- Owns the role list (names, ids, palette, persistence).
- Expresses the active role as placement presets:
fieldDefaults = { signature: { fieldColor, meta: { roleId } } }. - Reads the tag back from
field.meta(onfield-placed,getFormFields(), etc.).
Call them "signers", "approvers", "departments" — the viewer doesn't care, and everything works identically over the embed bridge (setFieldDefaults()).
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
:source="pdfUrl"
:field-defaults="fieldDefaults"
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)
// The active role is just a set of placement presets. Every field the
// user places while it's active lands pre-colored and pre-tagged.
const fieldDefaults = computed<PlacedFieldDefaults>(() => {
const role = roles.value.find((r) => r.id === activeRoleId.value)
const preset = role
? { fieldColor: role.color, meta: { roleId: role.id } }
: {}
return {
signature: preset,
text: preset,
checkbox: preset,
radio: preset,
}
})
</script>
To recolor a role's existing fields (or retag them), loop over getFormFields() and patch matching fields with updateFormField(id, { fieldColor }) — color is a per-field property, so the host stays in full control.
fieldColor and meta are viewer-session properties. 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.
Four companion features complete the flow:
editablePlacedFields— placed fields keep management chrome in hand mode, so users can move/resize/delete them without form-edit mode. The chrome is pass-through ("handle = manage, body = use"): the field body stays usable — click-to-sign and typing keep working — while the grip above the field selects and moves it (right-click on the body also selects). Fields withreadOnly: trueget no chrome and render dimmed: they are inert for the current user, which is how a host locks the other role's fields in a multi-signer flow. Fields parsed from the source PDF stay untouched.fieldDefaults— static per-type presets applied to every placement, including a hostmetabag so fields are tagged the moment they land.- The
field-placedevent — fires with the createdFormFieldDefinition; patch it viaupdateFormField()for per-field properties like a unique name. - The
field-updated/field-removedevents — push user-driven edits (grip moves, resizes, popover deletes, Del key) to the host, so external field state never needs reconciliation. Host-drivenupdateFormField()/removeFormField()calls do not echo back. See Keeping host state in sync.
<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',
'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>
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',
fieldColor: '#1677ff',
meta: { roleId: 'signer' },
required: true,
})
viewer.value?.addFormField({
pageNumber: 1,
fieldType: 'signature',
rect: [50, 640, 250, 680],
fieldName: 'signature',
fieldColor: '#1677ff',
meta: { roleId: 'signer' },
promptText: 'Click to sign',
lockAction: 'all',
})
}