API

KViewer

API reference for the KViewer component

KViewer is the main component for rendering a single PDF document with annotation editing, form fields, and search.

Props

PropTypeDefaultDescription
sourcestring | Uint8Array | objectrequiredPDF source: URL string, raw bytes, or pdfjs-dist document init params
textLayerbooleanfalseEnable text selection overlay and text-based annotation tools
userNamestringundefinedAuthor name attached to new annotations. When set, it is written as the annotation author (/T) on PDF export; when omitted, no author is written. See Author attribution.
stampsStampDefinition[]undefinedCustom stamp definitions for the stamp tool
signatureHandlersSignatureHandlersundefinedCallbacks for loading, saving, and deleting signatures
viewModeViewMode'fit-width'Initial fit mode: 'fit-width', 'fit-page', or 'fit-height'
zoomnumber1Initial zoom multiplier (0.25 to 2)
readonlybooleanfalseDisable all annotation and form editing
shapeDetectionbooleanfalseAuto-detect checkbox shapes and create interactive form fields
activebooleantrueWhether this viewer captures global keyboard shortcuts (used internally by KViewerTabs)
formEditModebooleanundefinedForm-edit mode toggle. Supports v-model:form-edit-mode. See Form-Edit Mode.
roleColorsRecord<string, string>undefinedMap from roleId → CSS color used to color-code form fields in form-edit mode. The host owns the role list; the viewer just consumes a flat lookup table.
activeRoleIdstring | nullundefinedRole id new field placements are auto-tagged with. Supports v-model:active-role-id.
clickToSignbooleanfalseEnable kviewer's built-in click-to-sign flow on signature fields (saved signature or draw modal). When off, signature fields render as passive markers (icon + a localized "Signature" label) and clicking does nothing — a host-owned signing process stays the only entry point. The field's promptText only applies to the click-to-sign placeholder.
editablePlacedFieldsbooleanfalseShow selection chrome on user-placed form fields in hand mode, without entering form-edit mode — they can be moved, resized, and deleted (Del/Backspace). Fields parsed from the source PDF are unaffected.
fieldDefaultsPlacedFieldDefaultsundefinedPer-field-type presets merged into every field the user places via a placement tool (e.g. a signature promptText or lockAction). fieldName stays auto-generated — rename via the field-placed event. Programmatic addFormField() calls are unaffected.
scriptingbooleanfalseExecute embedded PDF JavaScript (field AA, calculate/format/validate, document/page Open). See Embedded JavaScript.
menuItemsViewerMenuItem[]undefinedExtra items appended to the default burger menu (after Download and the form-field-detection toggle). Ignored when the host supplies a custom #header slot. See Customization → Custom menu items.
toolsViewerToolEntry[]undefinedChooses which toolbar tools render and in what order, and interleaves custom buttons. When omitted, the default toolbar is rendered. Ignored when the host supplies a custom #header slot. See Configuring the toolbar.
Breaking: the built-in click-to-sign flow on signature fields used to be always on. It is now opt-in — pass :click-to-sign="true" to restore the previous behavior.

Slots

SlotDefaultDescription
headerBuilt-in ViewerBar toolbarReplace the entire top toolbar
footerEmptyAdd content below the viewer
tool-<name>A custom toolbar button, placed by a { type: 'slot', name } entry in tools. Scoped with { state } (the viewer state).

Default Header (ViewerBar)

The default header includes: menu (download), page settings (rotation, layout, fullscreen), zoom controls, hand tool, marquee tool, page info, search, tool properties (color, stroke), drawing tools, and action tools (undo/redo/eraser). There is no form-edit toggle button — drive form-edit mode through the API (v-model:form-edit-mode, setFormEditMode(), toggleFormEditMode()), e.g. from your own button or a menuItems entry.

Configuring the toolbar

By default the toolbar is fully built-in. Pass the tools prop to choose which buttons render, reorder them, and drop in custom buttons — without replacing the whole header via the #header slot.

Each entry is either a built-in tool ID (a string controlling presence and order) or a { type: 'slot' } object that renders a custom button from a #tool-<name> slot at that position.

<template>
  <KViewer
    :source="pdfUrl"
    :tools="['menu', 'pageSettings', 'separator', 'zoom', 'spacer', 'pageInfo', 'search']"
  />
</template>

Built-in tool IDs

Top row: menu, pageSettings, zoom, hand, marquee, pageInfo, search.

Second row (per button):

  • Tool properties: properties
  • Draw tools: freehand, freeHighlight, freeText, stamp, signature, rectangle
  • Form-field tools: formText, formCheckbox, formRadio, formSignature
  • Field-placement tools (any mode): placeText, placeCheckbox, placeRadio, placeSignature — the same placement tools as the form* IDs, but visible regardless of form-edit mode, and selecting them does not switch the viewer into form-edit mode. Use these to offer e.g. "add a signature field" right next to the draw tools. See Placing fields without form-edit mode.
  • Actions: undo, redo, eraser
  • Opt-in annotation tools with no default button: select, highlight, strikeout, underline, circle, note, arrow, cloud

Layout primitives (either row): separator (a vertical divider) and spacer (pushes everything after it to the right — this is how you reproduce the default right-aligned page-info/search group).

A flat array is split into the two physical rows automatically: each built-in goes to its home row, and separator/spacer/slot entries inherit the row of the entry before them. Write the array in visual order (top-row tools first, then second-row tools) and it just works.

To reproduce the default toolbar exactly:

const tools = [
  // top row
  'menu', 'pageSettings', 'separator', 'zoom',
  'separator', 'hand', 'marquee',
  'spacer', 'pageInfo', 'search',
  // second row
  'properties', 'spacer',
  'freehand', 'freeHighlight', 'freeText', 'stamp', 'signature', 'rectangle',
  'formText', 'formCheckbox', 'formRadio', 'formSignature',
  'spacer', 'undo', 'redo', 'eraser',
]

Custom buttons

Add a { type: 'slot', key, name?, row? } entry where you want the button, then fill the matching #tool-<name> slot (defaults to key). The slot is scoped with { state }, the viewer state — use it to drive the viewer (state.selectTool(...), read state.activeTool.value, etc.).

<template>
  <KViewer
    :source="pdfUrl"
    :tools="['menu', { type: 'slot', key: 'save', row: 'top' }, 'spacer', 'search']"
  >
    <template #tool-save="{ state }">
      <UButton icon="i-lucide-save" size="xs" variant="ghost" color="neutral" @click="save(state)" />
    </template>
  </KViewer>
</template>

Notes & limitations

  • tools controls buttons only — it never gates programmatic access. Any annotation type stays selectable via state.selectTool(...), the component methods, or the embed bridge whether or not it has a button.
  • readonly always wins: the entire second row, plus hand/marquee, are hidden in read-only mode regardless of tools.
  • The second row is split into columns on spacer. With two spacers (three columns) it lays out as a centered grid — properties left, tools centered, actions right — matching the default toolbar; otherwise it is a left-packed flex row.
  • Custom buttons rely on Vue slots, which cannot cross the embed iframe — they are unavailable to cross-origin React consumers via the bridge.
Migration: the toolbar no longer has a form-edit toggle button. Drive form-edit mode through the API instead — v-model:form-edit-mode, setFormEditMode(), or toggleFormEditMode() — wired to your own button or a menuItems entry.

Events

EventPayloadDescription
update:formEditModebooleanForm-edit mode changed. Backs v-model:form-edit-mode.
update:activeRoleIdstring | nullActive role changed. Backs v-model:active-role-id.
update:viewedPagesnumber[]The set of viewed pages changed. Payload is the ascending list of pages seen so far.
all-pages-readFires once when the final unseen page is viewed. Re-arms after resetViewedPages() or loading a new document.
field-placedFormFieldDefinitionThe user placed a form field via a placement tool (in or out of form-edit mode). Patch it with updateFormField() to set host-driven properties like the field name or lock rules.
signed-status-changed{ allSigned: boolean, fields: SignatureFieldStatus[] }A signature field's signed state changed (signed, cleared, added, removed, or renamed). Push-style counterpart to getSignedStatus() — gate e.g. an envelope-finalize action on allSigned instead of polling.
<template>
  <KViewer
    :source="pdfUrl"
    @all-pages-read="canSign = true"
    @update:viewed-pages="(pages) => (readCount = pages.length)"
  />
</template>

<script setup lang="ts">
const canSign = ref(false)
const readCount = ref(0)
</script>

Methods

Access these through a template ref:

<template>
  <KViewer ref="viewer" :source="pdfUrl" />
</template>

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

getAnnotations()

Returns all current annotations as a serializable array.

const annotations: IAnnotationStore[] = viewer.value?.getAnnotations()

importAnnotations(annotations, options?)

Restores previously saved annotations.

const result = await viewer.value?.importAnnotations(annotations, {
  mode: 'replace', // 'replace' | 'merge'
})
// result: { loaded: number, skipped: number }
OptionTypeDefaultDescription
mode'replace' | 'merge''replace'replace clears existing annotations first. merge adds alongside existing, skipping collisions.

exportPdf(options?)

Exports the PDF with annotations as a Uint8Array.

const bytes = await viewer.value?.exportPdf({
  flatten: true,
  download: false,
  preserveOriginalAnnotations: false,
})

See ExportPdfOptions for all options.

getFormFieldValues()

Returns all form field values.

const fields: FormFieldValue[] = viewer.value?.getFormFieldValues()

setFormFieldValue(fieldName, value)

Sets a form field value by field name.

viewer.value?.setFormFieldValue('email', 'user@example.com')
viewer.value?.setFormFieldValue('agree_terms', true)

addFormField(payload)

Adds a new form field programmatically. Returns the created FormFieldDefinition. The new field is exported into the PDF on exportPdf() like any field placed via the placement tool.

const def = viewer.value?.addFormField({
  pageNumber: 1,
  fieldType: 'text',
  rect: [50, 700, 250, 720],
  fieldName: 'firstName',
  required: true,
})

The rect is in PDF user-space coordinates with a bottom-left origin: [x1, y1, x2, y2]. fieldName is auto-generated when omitted; multiple widgets that share a fieldName (and fieldType) act as one PDF field — values mirror across them on edit, and radios with the same fieldName form one option group.

See AddFormFieldPayload for all properties.

updateFormField(id, patch)

Patches a form field by id. Accepts a partial FormFieldDefinition — typically rect, fieldName, readOnly, required, or any type-specific prop.

viewer.value?.updateFormField(def.id, { rect: [60, 700, 260, 720] })
viewer.value?.updateFormField(def.id, { readOnly: true })

removeFormField(id)

Removes a form field by id and drops its value.

viewer.value?.removeFormField(def.id)

getFormFields()

Returns all form-field definitions across the document (parsed from the source PDF, auto-detected, and programmatically/UI placed), flattened across pages.

const defs: FormFieldDefinition[] = viewer.value?.getFormFields()

setFieldDefaults(defaults)

Replaces the per-field-type presets applied to tool placements. Programmatic counterpart to the fieldDefaults prop.

viewer.value?.setFieldDefaults({
  signature: { promptText: 'Sign here', lockAction: 'all' },
})

setClickToSign(enabled)

Enables or disables the built-in click-to-sign flow at runtime. Programmatic counterpart to the clickToSign prop.

viewer.value?.setClickToSign(true)

getSignedStatus()

Returns true once every signature field in the document is signed — via a value applied in this session or a digital signature already present in the source PDF. Vacuously true when the document has no signature fields, so combine with getSignatureFieldStatus().length when you need "at least one signature". Use it to gate e.g. finalizing an envelope. For a push-style signal, listen to the signed-status-changed event.

const allSigned: boolean = viewer.value?.getSignedStatus()

getSignatureFieldStatus()

Returns the per-widget signed-state snapshot — see SignatureFieldStatus.

const fields = viewer.value?.getSignatureFieldStatus()
// [{ id, fieldName, pageNumber, signed, roleId? }, …]
const missing = fields.filter((f) => !f.signed)

formEditMode

Reactive Ref<boolean> for the current form-edit-mode state. Read with .value. See Form-Edit Mode.

const isEditing = viewer.value?.formEditMode.value

setFormEditMode(enabled)

Programmatically enter or leave form-edit mode.

viewer.value?.setFormEditMode(true)

toggleFormEditMode()

Flips form-edit mode. Returns the new state.

const next = viewer.value?.toggleFormEditMode()

allPagesRead()

Returns true once the user has viewed every page — each page has scrolled into the viewport at least once since the document loaded (or since the last resetViewedPages()). Use it to gate a "must read all pages before signing" action. See also the all-pages-read event for a push-style signal.

const canSign = viewer.value?.allPagesRead()

A page counts as viewed as soon as its top edge enters the viewport, computed from scroll/pan geometry rather than render state — so a fast scroll straight to the bottom still marks every page it passed over. Enforcing that the user actually dwelled on each page is intentionally not a goal; skipping is treated as the signer's own choice. A document that fits entirely on screen (no scrolling needed) reports true immediately.

getViewedPages()

Returns the page numbers viewed so far, ascending — handy for a "3 / 10 read" progress indicator.

const seen: number[] = viewer.value?.getViewedPages()

resetViewedPages()

Clears viewed-page tracking, e.g. to restart a signing session on the same document. Loading a new source resets tracking automatically.

viewer.value?.resetViewedPages()

getKonvaCanvasState()

Returns the raw Konva canvas state for each page (page number to serialized Konva JSON).

const state: Record<number, string> = viewer.value?.getKonvaCanvasState()

Usage Example

pages/editor.vue
<template>
  <div class="h-screen">
    <KViewer
      ref="viewer"
      :source="pdfUrl"
      text-layer
      user-name="Jane Doe"
      :stamps="stamps"
      :signature-handlers="signatureHandlers"
    />
  </div>
</template>

<script setup lang="ts">
const viewer = ref()
const pdfUrl = '/documents/contract.pdf'

const stamps = [
  { id: 'approved', name: 'Approved', imageUrl: '/stamps/approved.svg', width: 48, height: 48 },
]

const signatureHandlers = {
  onLoad: () => fetch('/api/signatures').then(r => r.json()),
  onSave: (imageUrl: string) => fetch('/api/signatures', {
    method: 'POST',
    body: JSON.stringify({ imageUrl }),
    headers: { 'Content-Type': 'application/json' },
  }).then(r => r.json()),
  onDelete: (id: string) => fetch(`/api/signatures/${id}`, { method: 'DELETE' }).then(() => {}),
}
</script>
Copyright © 2026