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.
stylusModebooleanundefinedStylus mode: a finger only pans and zooms, annotating (drawing, selecting, moving, erasing) needs a pen such as the Apple Pencil, or a mouse. Off by default — it is never switched on by device detection. When the prop is omitted, the user's toggle in the page-settings menu is remembered per browser. Supports v-model:stylus-mode. See Touch input.
clickToSignbooleanfalseEnable kviewer's built-in click-to-sign flow on signature fields (clicking opens the 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.
addStampToSignaturebooleanfalseAdd a locale-formatted signing timestamp below base64 signatures applied through the built-in click-to-sign flow. The signature and timestamp are kept within the signature field. Has no effect on programmatically assigned signature values.
editablePlacedFieldsbooleanfalseShow management chrome on user-placed form fields in hand mode, without entering form-edit mode. The chrome is pass-through: the field body stays usable (click-to-sign, typing) while a grip above the field selects/moves it ("handle = manage, body = use"); right-click on the body also selects. Fields can be resized via handles and deleted (Del/Backspace, or the trash button in the selection popover). Fields with readOnly: true get no chrome — they are inert for the current user. 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, lockAction, flatten, or a host meta bag). 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.
selectionItemsViewerSelectionItem[]undefinedCustom buttons and selects appended to the floating selection popover — shown when an annotation or a placed form field is selected. See Customizing the selection popover.
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, stylus mode, fullscreen), zoom controls, hand tool, page info, search, tool properties (color, stroke), the select tool, drawing tools, and action tools (undo/redo/eraser). The hand tool also rubber-band selects: a mouse drag on empty page space draws a marquee that multi-selects annotations (touch drags keep panning). The select tool is the touch counterpart of the hand tool: a finger moves and resizes annotations instead of panning — see Touch input. 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. marquee has no button in the default toolbar (hand mode already rubber-band selects with the mouse) — list it explicitly for a dedicated multi-select mode that also works with touch drags.

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

Customizing the selection popover

Selecting an annotation (or, with selection chrome enabled, a placed form field such as a signature field) shows a floating popover with the built-in actions — color, font size, edit text, and delete. Pass the selectionItems prop to append your own buttons and selects to that popover.

Unlike toolbar customization, this is a data-driven API (no slots): each item describes itself and every callback receives a ViewerSelectionContext telling you what is selected — kind: 'annotation' | 'field', the selected annotations, and the selected field. Use an item's visible(ctx) predicate to scope it to one selection kind.

<script setup lang="ts">
import type { ViewerSelectionItem } from 'kviewer'

const selectionItems: ViewerSelectionItem[] = [
  {
    type: 'button',
    key: 'comment',
    label: 'Add comment',
    icon: 'i-lucide-message-square',
    onSelect: (ctx) => openCommentDialog(ctx.annotations[0]),
    visible: (ctx) => ctx.kind === 'annotation',
  },
  {
    type: 'select',
    key: 'status',
    label: 'Status',
    items: [
      { label: 'Draft', value: 'draft' },
      { label: 'Final', value: 'final' },
    ],
    value: (ctx) => statusFor(ctx),
    onUpdate: (value, ctx) => setStatus(ctx, value),
  },
]
</script>

<template>
  <KViewer :source="pdfUrl" :selection-items="selectionItems" />
</template>

See ViewerSelectionItem for the full item shapes. Notes:

  • Items render between the built-in actions and the delete button, in array order.
  • A select's value is either a plain (reactive) string or a getter deriving the value from the current selection — use the getter form for per-annotation/per-field state.
  • The popover only appears for editable selections, so selectionItems never renders in readonly mode.
  • Like menuItems, the items carry callbacks — they cannot cross the embed iframe and are unavailable to cross-origin consumers via the bridge.

Events

See Events for payload details, firing rules, and the embed-bridge equivalents.

EventPayloadDescription
update:formEditModebooleanForm-edit mode changed. Backs v-model:form-edit-mode.
update:stylusModebooleanStylus mode changed. Backs v-model:stylus-mode.
update:viewedPagesnumber[]A page was viewed for the first time.
all-pages-read—The final unseen page was viewed.
field-placedFormFieldDefinitionThe user placed a form field with a placement tool.
field-updated(field, patch)The user moved, resized, or edited a field. Not fired for updateFormField().
field-removedFormFieldDefinitionThe user removed a field. Not fired for removeFormField().
signed-status-changed{ allSigned, fields }A signature field's signed state changed.
<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.

printPdf(options?)

Exports the document and opens the browser's print dialog on the resulting PDF. Because the real (vector) PDF is handed to the browser's native print pipeline — with annotations and form values baked in — output is full print quality, unlike printing the page (Ctrl+P), which would rasterize the on-screen canvases at screen resolution.

await viewer.value?.printPdf({ flatten: true })

Accepts the same options as exportPdf (minus download/fileName). Also available as a built-in Print entry in the viewer's burger menu.

On iOS/iPadOS the PDF opens in a new tab instead (Safari can't print a multi-page PDF from a frame); the user prints from the native preview via the share sheet. If the viewer runs inside a sandboxed iframe, this fallback needs allow-popups.

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)

setAddStampToSignature(enabled)

Enables or disables the signing timestamp below signatures applied through the built-in click-to-sign flow. Programmatic counterpart to the addStampToSignature prop and available through the embed bridge. It is disabled by default.

viewer.value?.setAddStampToSignature(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 }, …]
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()

stylusMode

Reactive Ref<boolean> for the current stylus-mode state. Read with .value. See Touch input.

const penOnly = viewer.value?.stylusMode.value

setStylusMode(enabled)

Turn stylus mode on or off. Programmatic counterpart to the stylusMode prop. Available on the embed bridge as setStylusMode() / getStylusMode(), with a stylusMode-changed event.

viewer.value?.setStylusMode(true)

toggleStylusMode()

Flips stylus mode. Returns the new state.

const next = viewer.value?.toggleStylusMode()

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