KViewer
KViewer is the main component for rendering a single PDF document with annotation editing, form fields, and search.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
source | string | Uint8Array | object | required | PDF source: URL string, raw bytes, or pdfjs-dist document init params |
textLayer | boolean | false | Enable text selection overlay and text-based annotation tools |
userName | string | undefined | Author name attached to new annotations |
stamps | StampDefinition[] | undefined | Custom stamp definitions for the stamp tool |
signatureHandlers | SignatureHandlers | undefined | Callbacks for loading, saving, and deleting signatures |
viewMode | ViewMode | 'fit-width' | Initial fit mode: 'fit-width', 'fit-page', or 'fit-height' |
zoom | number | 1 | Initial zoom multiplier (0.25 to 2) |
readonly | boolean | false | Disable all annotation and form editing |
shapeDetection | boolean | false | Auto-detect checkbox shapes and create interactive form fields |
active | boolean | true | Whether this viewer captures global keyboard shortcuts (used internally by KViewerTabs) |
formEditMode | boolean | undefined | Form-edit mode toggle. Supports v-model:form-edit-mode. See Form-Edit Mode. |
roleColors | Record<string, string> | undefined | Map 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. |
activeRoleId | string | null | undefined | Role id new field placements are auto-tagged with. Supports v-model:active-role-id. |
scripting | boolean | false | Execute embedded PDF JavaScript (field AA, calculate/format/validate, document/page Open). See Embedded JavaScript. |
menuItems | ViewerMenuItem[] | undefined | Extra 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. |
tools | ViewerToolEntry[] | undefined | Chooses 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
| Slot | Default | Description |
|---|---|---|
header | Built-in ViewerBar toolbar | Replace the entire top toolbar |
footer | Empty | Add 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
toolscontrols buttons only — it never gates programmatic access. Any annotation type stays selectable viastate.selectTool(...), the component methods, or the embed bridge whether or not it has a button.readonlyalways wins: the entire second row, plushand/marquee, are hidden in read-only mode regardless oftools.- The second row is split into columns on
spacer. With two spacers (three columns) it lays out as a centered grid —propertiesleft, 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.
v-model:form-edit-mode, setFormEditMode(), or toggleFormEditMode() — wired to your own button or a menuItems entry.Events
| Event | Payload | Description |
|---|---|---|
update:formEditMode | boolean | Form-edit mode changed. Backs v-model:form-edit-mode. |
update:activeRoleId | string | null | Active role changed. Backs v-model:active-role-id. |
update:viewedPages | number[] | The set of viewed pages changed. Payload is the ascending list of pages seen so far. |
all-pages-read | — | Fires 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 }
| Option | Type | Default | Description |
|---|---|---|---|
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
<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>