React Components
inklayer-react provides two top-level components: PdfAnnotator and PdfViewer. Built on React 18+, using Zustand for state management, with ThemeProvider and i18n support.
Installation
npm install inklayer-react
Basic Import
import { PdfAnnotator, PdfViewer } from 'inklayer-react'
import 'inklayer-react/style'
// Types
import type { PdfAnnotatorProps, PdfViewerProps, User, IAnnotationStore } from 'inklayer-react'
PdfAnnotator
Full-featured PDF annotation component with built-in toolbar, annotation editing, sidebar, and save/load/export capabilities.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| url | string | URL | — | PDF file URL. Mutually exclusive with data |
| data | string | number[] | ArrayBuffer | Uint8Array | Uint16Array | Uint32Array | — | PDF binary data. Mutually exclusive with url |
| appearance | 'auto' | 'dark' | 'light' | 'auto' | UI appearance mode. auto follows system theme |
| theme | See theme color table below | 'violet' | UI theme color |
| title | React.ReactNode | 'PDF ANNOTATOR' | Annotator title |
| locale | 'zh-CN' | 'en-US' | 'zh-CN' | UI language |
| initialScale | 'auto' | 'page-actual' | 'page-fit' | 'page-width' | number | 'auto' | Initial zoom scale. 'page-actual' = actual size, 'page-fit' = fit page, 'page-width' = page width mode |
| layoutStyle | React.CSSProperties | undefined | Container styles. Must be explicitly provided, otherwise component height is 0. See note below |
| user | { id: string, name: string } | { id: 'null', name: 'unknown' } | Current user info for annotation ownership |
| enableNativeAnnotations | boolean | false | Enable PDF.js native annotation rendering |
| enableRange | boolean | 'auto' | 'auto' | PDF range loading mode. 'auto' auto-detects |
| defaultOptions | DeepPartial<PdfAnnotatorOptions> | — | Default annotation options (colors, signature, stamp) |
| initialAnnotations | Annotation[] | [] | Initial annotation data for restoring saved annotations |
| defaultShowAnnotationsSidebar | boolean | false | Whether annotation sidebar is expanded by default |
| actions | ReactNode | ((props: ActionsProps) => ReactNode) | — | Custom actions area, rendered on the right side of toolbar. Supports function form receiving operation context |
Theme Color Options
| Color Value | Description |
|---|---|
gray | Gray |
gold | Gold |
bronze | Bronze |
brown | Brown |
yellow | Yellow |
amber | Amber |
orange | Orange |
tomato | Tomato |
red | Red |
ruby | Ruby |
crimson | Crimson |
pink | Pink |
plum | Plum |
purple | Purple |
violet | Violet (default) |
iris | Iris |
indigo | Indigo |
blue | Blue |
cyan | Cyan |
teal | Teal |
jade | Jade |
green | Green |
grass | Grass |
lime | Lime |
mint | Mint |
sky | Sky |
Event Callbacks
| Event | Signature | Trigger |
|---|---|---|
| onSave | (annotations: Annotation[]) => void | User clicks save button |
| onLoad | () => void | PDF loading completed |
| onAnnotationAdded | (annotation: Annotation) => void | New annotation created |
| onAnnotationDeleted | (id: string) => void | Annotation deleted |
| onAnnotationSelected | (annotation: Annotation | null, isClick: boolean) => void | Annotation selected/deselected |
| onAnnotationUpdated | (annotation: Annotation) => void | Annotation updated (moved/edited) |
Full Example: Annotator with Persistence
import { PdfAnnotator } from 'inklayer-react'
import 'inklayer-react/style'
import type { Annotation } from 'inklayer-react'
import { useState, useCallback } from 'react'
export default function AnnotatorPage() {
const [annotations, setAnnotations] = useState<Annotation[]>([])
const handleSave = useCallback((annotations: Annotation[]) => {
setAnnotations(annotations)
// Send annotation data to backend
fetch('/api/annotations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(annotations),
})
}, [])
return (
<PdfAnnotator
url="/document.pdf"
locale="en-US"
layoutStyle={{ height: '96vh' }}
enableRange="auto"
defaultShowAnnotationsSidebar={true}
user={{ id: 'editor-1', name: 'Alice' }}
initialAnnotations={annotations}
defaultOptions={{
colors: ['#FF0000', '#00FF00', '#0000FF'],
signature: {
defaultSignature: [],
defaultFont: [{ label: 'Kai', value: 'STKaiti', external: false }]
},
stamp: { defaultStamp: [] }
}}
onSave={handleSave}
onLoad={() => console.log('PDF loaded')}
onAnnotationAdded={(a) => console.log('Added:', a.id)}
onAnnotationDeleted={(id) => console.log('Deleted:', id)}
actions={(props) => (
<>
<button onClick={() => props.save()}>Save</button>
<button onClick={() => console.log(props.getAnnotations())}>Get Annotations</button>
<button onClick={() => props.exportToExcel('export')}>Export Excel</button>
<button onClick={() => props.exportToPdf('export')}>Export PDF</button>
</>
)}
/>
)
}
⚠️ About layoutStyle
PdfAnnotator and PdfViewer use absolute positioning internally. The component size is entirely controlled by layoutStyle — it does not auto-fit to the parent container’s height. layoutStyle is a standard React.CSSProperties object, but all values must be passed as strings with CSS units:
// ✅ Correct
<PdfAnnotator layoutStyle={{ width: '100%', height: '600px' }} ... />
<PdfAnnotator layoutStyle={{ width: '100vw', height: '100vh' }} ... />
// ❌ Wrong (numeric values without units — the component can't calculate dimensions)
<PdfAnnotator layoutStyle={{ width: 800, height: 600 }} ... />
Common layout patterns:
// Pattern A: Full viewport
<PdfAnnotator layoutStyle={{ width: '100vw', height: '100vh' }} ... />
// Pattern B: Inside a container with fixed height
<div style={{ height: '600px' }}>
<PdfAnnotator layoutStyle={{ width: '100%', height: '100%' }} ... />
</div>
// Pattern C: Minus top navbar height
<PdfAnnotator layoutStyle={{ width: '100%', height: 'calc(100vh - 64px)' }} ... />
If the page is blank, the first thing to check: open React DevTools and inspect whether the
PdfAnnotatorDOM element has a computed height.
About onSave
onSave fires when the user clicks the “Save” button on the right side of the toolbar. The callback receives annotations in the complete Annotation[] format — save this array directly, no conversion needed:
const handleSave = (annotations: Annotation[]) => {
// annotations is already complete data, send directly
fetch('/api/annotations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(annotations),
})
}
Backend storage tip: If you need to structurally store annotation data on the backend, let the backend decide the storage format. The frontend only needs to get the complete
Annotation[]viaonSaveand send it.
PdfViewer
Lightweight PDF viewer without annotation editing. Ideal for read-only PDF viewing.
Props (inherits PdfAnnotator base props, plus the following)
| Prop | Type | Default | Description |
|---|---|---|---|
| showTextLayer | boolean | true | Show text selection layer |
| showAnnotations | boolean | false | Show existing annotations |
| defaultActiveSidebarKey | string | null | null | Default expanded sidebar panel key |
| toolbar | ReactNode | (ctx) => ReactNode | — | Custom toolbar (render props supported) |
| sidebar | SidebarPanel[] | — | Custom sidebar panels |
Viewer Full Example
import { PdfViewer } from 'inklayer-react'
import 'inklayer-react/style'
export default function ViewerPage() {
return (
<PdfViewer
url="/document.pdf"
showTextLayer={true}
showAnnotations={true}
layoutStyle={{ height: '96vh' }}
enableRange="auto"
defaultActiveSidebarKey={null}
actions={(context) => (
<>
<button onClick={() => context.print()}>Print</button>
<button onClick={() => context.download('file')}>Download</button>
<button onClick={context.toggleSidebar}>Toggle Sidebar</button>
</>
)}
toolbar={(context) => (
<div>
<button onClick={() => context.pdfViewer?.scrollPageIntoView({ pageNumber: 1 })}>
Go to Page 1
</button>
</div>
)}
sidebar={[{
key: 'custom-sidebar',
title: 'Custom Panel',
icon: <span>🔧</span>,
render: (context) => (
<div>
<button onClick={context.toggleSidebar}>Close</button>
</div>
)
}]}
onDocumentLoaded={(pdfViewer) => console.log('PDF loaded:', pdfViewer)}
onEventBusReady={(eventBus) => {
eventBus?.on('pagerendered', (e) => console.log('Page:', e.pageNumber))
}}
/>
)
}
SidebarPanel Type
The sidebar prop accepts SidebarPanel[], defining custom sidebar panels:
interface SidebarPanel {
key: string // Panel unique key
title: React.ReactNode // Panel title
icon: React.ReactNode // Panel icon
render: (context: PdfViewerContextValue) => React.ReactNode // Panel content render function
}
PdfViewerContextValue Type
The function form of actions, toolbar, sidebar receives a PdfViewerContextValue object:
interface PdfViewerContextValue {
pdfViewer: PDFViewer | null // PDFViewer instance
currentPage: number // Current page number (0-based)
totalPages: number // Total pages
scale: number // Current zoom scale
print: () => void // Print
download: (filename?: string) => void // Download
toggleSidebar: () => void // Toggle sidebar
scrollPageIntoView: (opts: { pageNumber: number }) => void // Scroll to page
}
ActionsProps Type
The actions prop can accept a function component that receives an ActionsProps object:
interface ActionsProps {
save: () => void // Trigger onSave callback
getAnnotations: () => Annotation[] // Get all current annotations
exportToExcel: (fileName?: string) => void // Export to Excel
exportToPdf: (fileName?: string) => void // Export to PDF
}
See the “Full Example” section above for usage.
PdfAnnotatorOptions Configuration
Customize default annotation behavior via the defaultOptions prop:
interface PdfAnnotatorOptions {
colors?: string[] // Available color list
signature?: {
colors?: string[] // Signature color options
type?: 'Draw' | 'Enter' | 'Upload' // Signature type (note capital first letter)
maxSize?: number // Max signature image file size (bytes)
accept?: string // Accepted image formats
defaultSignature?: string[] // Default signature list
defaultFont?: { // Default font list
label: string
value: string
external?: boolean
url?: string // External font URL
}[]
}
stamp?: {
maxSize?: number // Stamp image max file size
accept?: string // Accepted image formats
defaultStamp?: string[] // Default stamp list
editor?: { // Stamp editor config
defaultBackgroundColor?: string
defaultBorderColor?: string
defaultBorderStyle?: 'none' | 'solid' | 'dashed'
defaultTextColor?: string
defaultFont?: {
label: string
value: string
}[]
}
}
}
Note:
signature.typevalues are'Draw' | 'Enter' | 'Upload'(capitalized), NOT'draw' | 'text' | 'image'.