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
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.
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.

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
  • 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',
  '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.
<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()

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