Migration

Migrating from Syncfusion PDF Viewer

This guide moves a React application built on the Syncfusion React PDF Viewer (@syncfusion/ej2-react-pdfviewer) to @sdk-pdf/react. It covers both Syncfusion deployment modes: standalone, which renders in the browser with PDFium assets from resourceUrl, and server-backed, which sends the work to a web service at serviceUrl. PdfViewerComponent is a complete viewer with toolbars, panels, and dialogs, extended by the feature modules you inject; @sdk-pdf/react ships the document engine and the page view and leaves the rest of the UI to you. Most of the migration is rebuilding the built-in UI you rely on as React components, then moving your calls through the viewer ref to hooks. Work through the checklist below; each step links to the section that covers it.

Beta: @sdk-pdf/react is in beta, and its exports and APIs may change between releases. Pin the exact package version and review the changes when you update.

Migration checklist

Overview

How the two integrations differ

The Syncfusion PDF Viewer is a packaged viewer component:

  • PdfViewerComponent renders the whole viewer: the primary toolbar, the navigation panel with thumbnails and bookmarks, the annotation and form designer toolbars, the Organize Pages dialog, the context menu, and dialogs such as the password prompt and error messages.
  • Features are modules. Each one is registered with <Inject services={[...]} /> and switched with an enable* prop (enableToolbar, enableAnnotation, enableFormDesigner, enablePageOrganizer, and more), most of them on by default. A module that isn't injected isn't available: without ThumbnailView, thumbnailView is undefined.
  • You reach the API through a ref to the component: instance members (load, saveAsBlob, download, undo, pageCount) and module objects (navigation, magnification, annotation, formDesigner, thumbnailView, pageOrganizer). Changes arrive as component events such as documentLoad, pageChange, and zoomChange.
  • There are two deployment modes. Standalone mode renders in the browser with PDFium (pdfium.js and pdfium.wasm) loaded from resourceUrl, from the CDN or from your public folder. Server-backed mode renders and processes on a .NET web service at serviceUrl; since the 2026 Vol 2 release it gets no new features.
  • The app registers a license key with registerLicense from @syncfusion/ej2-base and imports its styles from a theme package, such as @syncfusion/ej2-tailwind3-theme.
  • Pages are 1-based numbers, zoom is a percentage (10 to 400 by default), and annotations and form fields are objects in collections (annotationCollection, formFieldCollections) that you edit through module methods and can export as JSON, XFDF, or FDF.

@sdk-pdf/react is a library: the SDK owns the document, and you own the UI.

  • There are no prebuilt toolbars, panels, or dialogs. Viewport is the main view and PageCanvas renders single pages; everything around them is your React code driven by hooks.
  • PdfProviders hosts the state and configuration inside your React tree. There are no modules to inject and no instance to hold: components under it reach the document through hooks.
  • Rendering and editing run in the browser from the installed package, with no asset folder to host and no server mode. The SDK renders unstyled, and your design system supplies the look through the theme configuration.
  • Edits don't rewrite the PDF file. Each change is an operation in a CRDT, which powers the history; useDocumentSaving builds the final PDF when you need it.
  • Pages and blocks are addressed by session-scoped ids, the zoom is a scale factor, and positions are user-space units with the origin at the bottom-left corner of the page.
ConcernSyncfusion PDF Viewer@sdk-pdf/react
UIPrebuilt toolbars, panels, and dialogs, configured with props (toolbarSettings, enable*)Viewport and PageCanvas only; you build toolbars, panels, and dialogs
FeaturesModules registered with Inject and switched with enable* propsHooks, all available under PdfProviders
RenderingStandalone: PDFium in the browser from resourceUrl; server-backed: a web service at serviceUrlIn the browser, from the package
DeploymentLicense key, theme package CSS, PDFium assets or a web serviceThe package from a private registry
State accessInstance members and module methods through a ref, component eventsState hooks that re-render, command hooks, and event hooks
Pages1-based numbersSession-scoped page ids
ZoomPercentages, 10 to 400 by defaultA scale factor, where 1 is 100%
FormsFields in formFieldCollections, updated with updateFormFieldsValueWidgets with a fieldName, values bound with useWidgetValue
AnnotationsMarkup, shape, measurement, free text, ink, stamp, and sticky note toolsExisting annotations display; image stamps, ink from code, and border restyling
TextFree text annotations on top of the pagePage text editable in place under the PDF_EDITOR content policy
PersistencesaveAsBlob() and download(); JSON, XFDF, and FDF for annotations and form dataprepareDocument() and downloadDocument() for the PDF

What this means for your code:

  1. Build the UI. Every toolbar item, panel, and dialog your users rely on becomes a React component built from the hooks in the API reference.
  2. Drop the ref. Calls through viewerRef.current become hook calls in the components that need them, and event props become state hooks or event hooks.
  3. Drop the modules and the deployment pieces: Inject, the enable* props, the PDFium assets or the web service, the license registration, and the theme CSS.
  4. Replace numbers and percentages with page ids and scale factors; convert at the edges, such as deep links and stored preferences.
  5. Rethink forms as widgets. Field objects become widgets, and the form designer becomes widget hooks.
  6. Plan for the annotation tools. Markup, shape, measurement, and sticky note tools have no equivalent; image stamps become image placement, and ink can be created from code.
  7. Replace exported annotation and form data with storing the PDF produced by prepareDocument().
API map

Rows marked No equivalent are covered in Features without an equivalent.

Syncfusion PDF Viewer@sdk-pdf/reactNotes
<PdfViewerComponent> with <Inject services={[...]} /><PdfProviders> and ViewportNo modules to register.
resourceUrl, resourcesLoaded, registerLicenseNothing to configureNo asset URL or license key.
serviceUrl, serverActionSettings, ajaxRequestSettings, retryCountNo equivalentEverything runs in the browser.
documentPath, load(document, password)loadPdf(Uint8Array, { password }) from useLoadPdfFetch the URL or decode the Base64 into bytes first.
Built-in password popupCatch InvalidPasswordError and ask yourself
documentLoad eventloadState.status === 'ready'
documentLoadFailed eventThe rejected loadPdf promiseSee Errors and logging.
unload(), documentUnload eventresetPdf() from useResetPdf
pageCountusePageCount
currentPageNumber, pageChange eventuseCurrentPageIdRe-renders with the id of the page in the middle of the viewport.
navigation.goToPage(pageNumber) and the first, previous, next, and last page methodsscrollToPage(pageId) from useScrollToPageResolve numbers through usePages.
getPageInfo(pageIndex)The width, height, and rotation of the usePages() entriesSame unit as points; rotation applied.
magnification.zoomTo(percent), zoomIn(), zoomOut(), zoomValue, zoomPercentageuseViewportScaleA factor, not a percentage.
magnification.fitToWidth(), fitToPage(), zoomModeuseViewportAutoFitScale, useViewportFitScaleOne-time fits.
minZoom, maxZoomzoomGestures on PdfProvidersBounds the pinch and wheel zoom.
zoomChange eventscale from useViewportScale in a useEffectzoomGestureEnd from useViewportEvent reports pinch and wheel zoom.
ThumbnailView, thumbnailView.openThumbnailPane(), thumbnailClick eventPageCanvas in your own sidebar
Organize Pages dialog, pageOrganizerSettings, pageOrganizer.openPageOrganizer()The page hooks in your own panelSee Page organizer.
extractPages(pages)prepareDocument({ pageIds })Saves only the listed pages, in the listed order.
Importing pages from another PDFNo equivalent
formFieldCollections, retrieveFormFields()useWidgets, useWidgetsGetterWidgets, not fields.
updateFormFieldsValue(field), clearFormFields()useWidgetValueOne widget per hook call.
resetFormFields()No equivalent
focusFormField(name)scrollToBlock(widgetId) from useScrollToBlockScrolls to the widget.
formFieldAdd, formFieldRemove, formFieldPropertiesChange, and other field eventsuseWidgets re-renders on changes; widgetInput from useViewportEvent reports a value the user changeduseDocumentRevision changes on any edit.
importFormFields(), exportFormFields(), exportFormFieldsAsObject()No equivalentprepareDocument() produces the filled PDF.
isRequired, enableFormFieldsValidation, validateFormFields eventNo equivalentValidate in your own submit flow.
formDesigner.addFormField(type, settings)createWidgets(props) from useWidgetsCreate
formDesigner.setFormFieldMode(type)The { type: 'addWidget', widgetProps } interaction toolPass PDF_EDITOR with the tool.
formDesigner.updateFormField(field, settings)setWidgetProps(ids, props) from useSetWidgetProps
formDesigner.deleteFormField(field)deleteBlocks(ids) from useBlockDelete
interactionMode, annotation.setAnnotationMode(type)useSetInteractionModeSee Editing and interaction tools.
enableAnnotation, enableFormDesigner, enableFormFields, designerModeA content policy from CONTENT_POLICY_PRESET, or a custom one
annotation.addAnnotation(), editAnnotation(), annotationCollectionNo equivalentInk: useInkAnnotationCreate; image stamps: useStampAnnotationCreate; selected shapes, lines, and ink: border color and width through useSelectionAnnotationProps. See Annotations.
exportAnnotation(), exportAnnotationsAsObject(), importAnnotation()No equivalentNo JSON or XFDF.
annotationAdd, annotationRemove, annotationPropertiesChange eventsaddToolApplied, blocksDragEnd, and blockResizeDragEnd from useViewportEvent; useDocumentRevision for any changeOnly for what the user does in Viewport.
toolbarSettings, enableToolbar, toolbar.showToolbarItem(), toolbarClick eventYour own componentsSee Display the document.
Theme packages, dark themesThe theme configuration, see Theming and customization
undo(), redo()useUndoRedoWith canUndo and canRedo.
isDocumentEditeduseDocumentRevision, useDocumentRevisionGetterCompare with the revision you last saved.
saveAsBlob()prepareDocument() from useDocumentSavingResolves with a Uint8Array, not a Blob.
download(), downloadFileNamedownloadDocument()No file name option.
downloadStart, downloadEnd events, annotationSettings.skipDownloadNo equivalent
showNotificationDialog, showNotificationPopup()Your own error UIThe SDK shows no dialogs.
destroy(), enableLocalStorageNo dispose call; unsubscribe log listeners yourselfSee Cleanup and lifecycle.
extractText(pageIndex, options)getParagraphsByPage(pageId) from useProgrammaticEditorParagraph text and bounds.
textSearch.searchText(), searchNext(), searchPrevious()useDocumentSearchHighlights the matches and scrolls to the active one.
Print module, BookmarkView moduleNo equivalent
convertClientPointToPagePoint(), convertPagePointToClientPoint()No equivalent

Install and set up

Before: Syncfusion PDF Viewer

The Syncfusion PDF Viewer installs as @syncfusion/ej2-react-pdfviewer, takes its styles from a theme package, and, in standalone mode, loads PDFium from a CDN or from assets you copy into your app:

npm install @syncfusion/ej2-react-pdfviewer @syncfusion/ej2-tailwind3-theme
# Only to host PDFium yourself instead of using the CDN.
cp -R ./node_modules/@syncfusion/ej2-pdfviewer/dist/ej2-pdfviewer-lib \
  public/assets/ej2-pdfviewer-lib
@import '../node_modules/@syncfusion/ej2-tailwind3-theme/styles/pdfviewer/index.css';

The license key is registered once, and the component lists the modules it uses:

import { registerLicense } from '@syncfusion/ej2-base'
import {
  Annotation,
  FormDesigner,
  FormFields,
  Inject,
  Magnification,
  Navigation,
  PageOrganizer,
  PdfViewerComponent,
  TextSelection,
  ThumbnailView,
  Toolbar,
} from '@syncfusion/ej2-react-pdfviewer'

registerLicense('YOUR_LICENSE_KEY')

// The PDFium assets must match the installed version.
export const RESOURCE_URL =
  'https://cdn.syncfusion.com/ej2/35.1.37/dist/ej2-pdfviewer-lib'

export function App() {
  return (
    <PdfViewerComponent
      id="container"
      documentPath="https://example.com/files/contract.pdf"
      // Standalone mode; server-backed integrations set serviceUrl instead.
      resourceUrl={RESOURCE_URL}
      style={{ height: '100vh' }}
    >
      <Inject
        services={[
          Toolbar,
          Magnification,
          Navigation,
          ThumbnailView,
          TextSelection,
          Annotation,
          FormFields,
          FormDesigner,
          PageOrganizer,
        ]}
      />
    </PdfViewerComponent>
  )
}

The other Before snippets in this guide reuse RESOURCE_URL.

After: @sdk-pdf/react

Add the @sdk-pdf scope to an .npmrc next to your package.json, keep the token in an environment variable, and install; Getting started has the details:

@sdk-pdf:registry=https://registry.sdk.pdf.net
//registry.sdk.pdf.net/:_authToken=${PDF_NET_REGISTRY_TOKEN}
npm install @sdk-pdf/react

Set up the project as Getting started describes: import the stylesheet, render the SDK on the client only, alias canvas in the bundler, and turn off React Strict Mode.

Mount PdfProviders where PdfViewerComponent was. Its props must keep their identity between renders, so define tools outside the component and memoize the configuration:

import { useMemo } from 'react'
import { CONTENT_POLICY_PRESET, InteractionTool, PdfProviders } from '@sdk-pdf/react'
import '@sdk-pdf/react/style.css'

// Like interactionMode="TextSelection": native text selection for reading and copying.
const textSelectTool: InteractionTool = { type: 'textSelect' }
// Select, move, and resize blocks.
const selectTool: InteractionTool = { type: 'select' }

export function App({ canEdit }: { canEdit: boolean }) {
  // The PDF application re-renders whenever a PdfProviders prop changes identity.
  const config = useMemo(
    () => ({
      theme: { viewportPadding: 16 },
      // Both apply when the viewport first mounts; switch later with
      // useSetInteractionMode.
      initialInteractionTool: canEdit ? selectTool : textSelectTool,
      initialContentPolicy: canEdit
        ? CONTENT_POLICY_PRESET.PDF_EDITOR
        : CONTENT_POLICY_PRESET.PDF_VIEWER,
    }),
    [canEdit]
  )

  return (
    <PdfProviders {...config}>
      <DocumentLoader url="/files/contract.pdf" />
    </PdfProviders>
  )
}

What to know

  • No assets, no service, no license key in code. The install is the registry scope and the package. resourceUrl, the copied ej2-pdfviewer-lib folder, serviceUrl and its web service, and registerLicense have no counterpart. A server-backed integration keeps only the endpoints that serve and store the PDF bytes.
  • No modules. Inject and the enable* props go away. Every hook works under PdfProviders, and a feature your users shouldn't see is a control you don't render; the content policy decides what users may change inside Viewport.
  • No ref. The ref to PdfViewerComponent, or the ej2_instances lookup, goes away: any component under PdfProviders reaches the document through hooks.
  • Both default to editing. The Syncfusion viewer enables annotations, the form designer, form fields, and the page organizer by default, and PdfProviders defaults to the select tool and CONTENT_POLICY_PRESET.PDF_EDITOR, where text, annotations, links, and widgets are all editable. Where your app turned those modules off for read-only users, start with PDF_VIEWER (text and annotations read-only, links and widgets interactive) and the textSelect tool, as above.

Load and reset a document

Before: Syncfusion PDF Viewer

documentPath loads the first document, and load(document, password) loads another one at runtime from a URL, a Base64 data URI, or a Uint8Array. Encrypted files get the built-in password popup. Loading reports through documentLoad and documentLoadFailed, whose arguments carry documentName, isPasswordRequired, and the incorrect password, and unload() closes the document:

import { useRef } from 'react'
import {
  Inject,
  Navigation,
  PdfViewerComponent,
  Toolbar,
} from '@syncfusion/ej2-react-pdfviewer'

function Viewer() {
  const viewerRef = useRef(null)

  async function openFromServer() {
    const response = await fetch('/api/documents/contract.pdf', {
      headers: { Authorization: `Bearer ${token}` },
    })
    const blob = await response.blob()
    // The second argument is the password; the popup asks for it when it's missing or
    // wrong.
    viewerRef.current?.load(URL.createObjectURL(blob), null)
  }

  return (
    <>
      <button onClick={openFromServer}>Open</button>
      <button onClick={() => viewerRef.current?.unload()}>Close</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        documentLoad={(args) => showDocumentUi(args.documentName)}
        documentLoadFailed={(args) => showError(args.documentName)}
        style={{ height: '100vh' }}
      >
        <Inject services={[Toolbar, Navigation]} />
      </PdfViewerComponent>
    </>
  )
}

In standalone mode, a load call made before the PDFium assets are ready can fail, so integrations that load right away wait for the resourcesLoaded event.

After: @sdk-pdf/react

loadPdf from useLoadPdf takes the file bytes, and the load state decides what to render. Fetching the file is your own code, with whatever headers your server needs:

import { useRef, useState } from 'react'
import { InvalidPasswordError, useLoadPdf } from '@sdk-pdf/react'

function DocumentLoader({ url }: { url: string }) {
  const [loadState, loadPdf] = useLoadPdf()
  const bytesRef = useRef<Uint8Array | null>(null)
  const [needsPassword, setNeedsPassword] = useState(false)
  const [loadError, setLoadError] = useState<unknown>()

  async function open(password?: string) {
    setLoadError(undefined)

    try {
      // Replaces documentPath: fetch the file once and keep the bytes for a password
      // retry.
      if (!bytesRef.current) {
        const response = await fetch(url)
        if (!response.ok)
          throw new Error(`The document request failed with ${response.status}`)
        bytesRef.current = new Uint8Array(await response.arrayBuffer())
      }

      // loadPdf reads from the buffer it receives for the document's lifetime, so pass a
      // copy.
      await loadPdf(bytesRef.current.slice(), { password })
      setNeedsPassword(false)
    } catch (error) {
      if (error instanceof InvalidPasswordError) {
        // Replaces the built-in password popup: ask, then call open(password) again.
        setNeedsPassword(true)
      } else {
        // Replaces documentLoadFailed and the built-in error message box.
        setLoadError(error)
      }
    }
  }

  if (needsPassword)
    return <PasswordPrompt onSubmit={(password) => void open(password)} />
  if (loadError) return <LoadError error={loadError} onRetry={() => void open()} />
  if (loadState.status === 'initial')
    return <button onClick={() => void open()}>Open</button>
  if (loadState.status === 'loading') return <p>Loading the document…</p>

  return <PdfApplication />
}

useResetPdf replaces unload():

import { useResetPdf } from '@sdk-pdf/react'

function CloseDocumentButton() {
  const resetPdf = useResetPdf()

  // The load state returns to 'initial', and DocumentLoader shows its Open button again.
  return <button onClick={() => resetPdf()}>Close</button>
}

What to know

  • Bytes, not paths. loadPdf takes a Uint8Array. A URL becomes a fetch, a Blob or File becomes new Uint8Array(await blob.arrayBuffer()), and a Base64 string is decoded to bytes first. A server-backed documentPath that names a file on the web service becomes a request to your own endpoint.
  • Keep a copy for retries. The document reads from the given buffer for its whole lifetime, and another loadPdf call can detach it. To reuse the bytes for a password retry, pass a copy: loadPdf(data.slice(), { password }).
  • Passwords are your UI. Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with InvalidPasswordError. There is no popup: catch the rejection, ask, and call loadPdf again.
  • No events to wire. documentLoad becomes the awaited loadPdf or loadState.status === 'ready', and documentLoadFailed becomes the rejection. With no resourceUrl, there is no resourcesLoaded step before the first load.
  • Closing invalidates ids. After resetPdf(), page and block ids, widget snapshots, and document revisions from the old document are no longer valid. For an open-another-document action, call resetPdf() and then loadPdf.

Display the document

Before: Syncfusion PDF Viewer

The component renders the document with its toolbars and panels. Integrations pick the primary toolbar items, handle custom items in toolbarClick, hide toolbars with props or with toolbar.showToolbar(false), and style everything with a theme package:

<PdfViewerComponent
  ref={viewerRef}
  resourceUrl={RESOURCE_URL}
  // Only these items, in this order; a custom item is an object with an id.
  toolbarSettings={{
    toolbarItems: [
      'OpenOption',
      'PageNavigationTool',
      'MagnificationTool',
      { id: 'fit_width', text: 'Fit to width', align: 'Center' },
      'DownloadOption',
    ],
  }}
  toolbarClick={(args) => {
    if (args.item.id === 'fit_width') viewerRef.current?.magnification.fitToWidth()
  }}
  enableNavigationToolbar={false}
  style={{ height: '640px' }}
>
  <Inject services={[Toolbar, Magnification, Navigation]} />
</PdfViewerComponent>

For a fully custom toolbar, integrations set enableToolbar={false} and call the viewer API from their own buttons, which is the shape every toolbar takes with @sdk-pdf/react.

After: @sdk-pdf/react

Viewport renders the pages, and your components render everything else. A typical shell puts a toolbar above the viewport and a sidebar beside it, as Building a PDF application walks through:

import { Config, useViewportAutoFitScale, Viewport } from '@sdk-pdf/react'

// A fuller theme than the one in App: pass it as PdfProviders' `theme` prop.
// Defined outside components, so its identity is stable.
export const theme: Config['theme'] = {
  viewportPadding: [24, 16],
  classNames: { scrollViewport: 'pdf-viewport', page: 'pdf-page' },
  styles: { scrollViewport: { '--selection-color': '#0b5cad' } as React.CSSProperties },
}

function PdfApplication() {
  // Replaces zoomMode="FitToWidth": fits once per loaded document.
  useViewportAutoFitScale('width')

  return (
    <div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', height: '100vh' }}>
      {/* Replaces the primary toolbar: the controls from the sections below. */}
      <header style={{ display: 'flex', gap: 8 }}>
        <PageNavigation />
        <ZoomControls />
        <HistoryControls />
        <SaveControls />
      </header>
      <div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', minHeight: 0 }}>
        {/* Replaces the navigation panel. */}
        <ThumbnailsSidebar />
        {/* The viewport fills its parent, so the parent needs an explicit size. */}
        <div style={{ minHeight: 0 }}>
          <Viewport />
        </div>
      </div>
    </div>
  )
}

What to know

  • There is nothing to hide. toolbarItems, enableToolbar, enableNavigationToolbar, and the show and hide methods of the toolbar module have no counterpart: every control exists only if you build it. Selection, text editing, form filling, links, and zoom gestures work inside Viewport, limited by the content policy.
  • Toolbar clicks become handlers. Custom toolbar items and the toolbarClick branches on args.item.id become onClick handlers on your own buttons, which call the hooks shown in this guide.
  • Theming is CSS you own. The Material 3, Fluent 2, Bootstrap 5, and Tailwind theme packages and their dark variants have no counterpart. theme.classNames and theme.styles target the SDK's elements (scrollViewport, page, pageCanvas, resizable, widgetOverlay, and more), and CSS variables such as --selection-color and --widget-overlay-background tune the selection and widget visuals; Theming and customization lists them.
  • Size the parent. Where PdfViewerComponent takes a height or a style, Viewport fills its parent element, so give the parent an explicit size.
  • Render only when ready. Viewport throws DocumentNotLoadedError when no document is loaded.

Pages and navigation

Before: Syncfusion PDF Viewer

The navigation module moves between 1-based page numbers, pageCount and currentPageNumber read the position, and pageChange reports changes:

import { useRef, useState } from 'react'
import { Inject, Navigation, PdfViewerComponent } from '@syncfusion/ej2-react-pdfviewer'

function Viewer() {
  const viewerRef = useRef(null)
  const [pageNumber, setPageNumber] = useState(1)
  const [pageCount, setPageCount] = useState(0)

  return (
    <>
      <button onClick={() => viewerRef.current?.navigation.goToPreviousPage()}>
        Previous
      </button>
      <span>
        {pageNumber} of {pageCount}
      </span>
      <button onClick={() => viewerRef.current?.navigation.goToNextPage()}>Next</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        documentLoad={() => setPageCount(viewerRef.current.pageCount)}
        pageChange={(args) => setPageNumber(args.currentPageNumber)}
        style={{ height: '640px' }}
      >
        <Inject services={[Navigation]} />
      </PdfViewerComponent>
    </>
  )
}

After: @sdk-pdf/react

usePages returns the pages in document order, so a page number is an index in that list plus one. useScrollToPage scrolls to a page id, and useCurrentPageId re-renders with the page in the middle of the viewport:

import { useCurrentPageId, usePages, useScrollToPage } from '@sdk-pdf/react'

function PageNavigation() {
  const pages = usePages()
  // Replaces currentPageNumber and pageChange.
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()

  // Until there is a current page, treat the first page as current.
  const index =
    currentPageId === undefined ? 0 : pages.findIndex((page) => page.id === currentPageId)

  // Replaces navigation.goToPage(pageNumber): resolve the 1-based number to an id first.
  function scrollToPageNumber(pageNumber: number) {
    const page = pages[pageNumber - 1]
    if (page) scrollToPage(page.id, { behavior: 'smooth' })
  }

  return (
    <div role="toolbar">
      <button disabled={index <= 0} onClick={() => scrollToPageNumber(1)}>
        First
      </button>
      {/* The current page number is index + 1, so the previous one is index. */}
      <button disabled={index <= 0} onClick={() => scrollToPageNumber(index)}>
        Previous
      </button>
      <span>
        {index === -1 ? '–' : index + 1} of {pages.length}
      </span>
      <button
        disabled={index === -1 || index >= pages.length - 1}
        onClick={() => scrollToPageNumber(index + 2)}
      >
        Next
      </button>
      <button
        disabled={index === -1 || index >= pages.length - 1}
        onClick={() => scrollToPageNumber(pages.length)}
      >
        Last
      </button>
    </div>
  )
}

When you need a page's number inside a handler, such as for analytics, getPageIndex(pageId) from usePageIndexGetter resolves an id to its zero-based index without re-rendering on order changes; usePageCount replaces pageCount.

What to know

  • Ids survive reordering; numbers don't. An id keeps pointing at the same page while pages are added, removed, and rearranged by the user, where a number would drift to another page. Ids are session-scoped: persist page numbers, not ids, and resolve them once the document is ready.
  • Invalid targets throw. goToPage with a number out of range keeps the current page. scrollToPage with an unknown or deleted id throws InvalidInputError, so resolve the number first, as scrollToPageNumber above does.
  • Scroll options. scrollToPage(pageId, options?) takes behavior ('auto', 'smooth', or 'instant'), vertical ('top' by default), and horizontal ('nearest' by default).
  • Page size. getPageInfo(pageIndex) becomes the width, height, and rotation of a usePages() entry. The sizes are user-space units, one unit being 1/72 of an inch like a point, with the rotation applied.
  • Links and bookmarks. Links work inside Viewport when the content policy's links level is 'interactive'. The bookmarks panel (BookmarkView), hyperlinkClick, and hyperlinkOpenState have no equivalent.

Zoom and fit modes

Before: Syncfusion PDF Viewer

Zoom is a percentage, 10 to 400 by default and clamped to minZoom and maxZoom. zoomMode sets the initial fit, and the magnification module zooms and fits from your controls:

<PdfViewerComponent
  ref={viewerRef}
  resourceUrl={RESOURCE_URL}
  // 'Default', 'FitToWidth', or 'FitToPage'.
  zoomMode="FitToWidth"
  minZoom={50}
  maxZoom={300}
  zoomChange={(args) => persistZoom(args.zoomValue)}
  style={{ height: '640px' }}
>
  <Inject services={[Toolbar, Magnification, Navigation]} />
</PdfViewerComponent>
viewerRef.current?.magnification.zoomTo(150)
viewerRef.current?.magnification.zoomIn()
viewerRef.current?.magnification.fitToPage()
viewerRef.current?.magnification.fitToWidth()

After: @sdk-pdf/react

useViewportScale reads and sets the scale, useViewportFitScale calculates fit scales on demand, and useViewportAutoFitScale (in PdfApplication above) fits once per loaded document:

import { useEffect } from 'react'
import { useViewportFitScale, useViewportScale } from '@sdk-pdf/react'

function ZoomControls() {
  const [scale, setScale] = useViewportScale()
  const getFitScale = useViewportFitScale()

  // Replaces zoomChange: every scale change, whatever the source.
  useEffect(() => persistZoom(scale), [scale])

  function fitPage() {
    const { fitWidthScale, fitHeightScale } = getFitScale()
    // The smaller scale fits the pages in both directions, like fitToPage().
    setScale(Math.min(fitWidthScale, fitHeightScale))
  }

  return (
    <div role="toolbar">
      <button onClick={() => setScale(scale / 1.25)}>Zoom out</button>
      {/* The scale is a factor: 1 is 100%. */}
      <span>{Math.round(scale * 100)}%</span>
      <button onClick={() => setScale(scale * 1.25)}>Zoom in</button>
      {/* Call the getter inside the handler: the result reflects the viewport size at
         that moment. */}
      <button onClick={() => setScale(getFitScale().fitWidthScale)}>Fit width</button>
      <button onClick={fitPage}>Fit page</button>
    </div>
  )
}

What to know

  • Percentages become factors. setScale takes a positive finite number, not a percentage, and throws InvalidInputError otherwise; scale 1 is one user-space unit per CSS pixel. Compare the page size on screen with a test document before converting zoom levels you stored as Syncfusion percentages.
  • Fits are one-time calculations. useViewportAutoFitScale takes 'width' or 'height' and applies when a document loads; it doesn't re-apply on viewport resizes or manual zooming. zoomMode="FitToPage" has no automatic counterpart: fit the page from a control, as fitPage above does.
  • What the fits measure. fitWidthScale fits the widest page of the document into the viewport width and fitHeightScale the tallest page into its height, both accounting for viewportPadding.
  • Zoom range and gestures. Pinch and wheel zoom are built in and tuned with zoomGestures on PdfProviders: minScale (0.5 by default) and maxScale (4) take the place of minZoom and maxZoom for the gestures, and the wheel speed is configurable too.
  • Zoom around a point. setScale(scale, pivot?) keeps the document still at pivot, a point { x, y } given as fractions of the viewport area (0 the top or left edge, 1 the bottom or right edge), the center by default. The third item of useViewportScale(), animateScale(scale, pivot?), animates the change and resolves when it ends.
  • The scale updates after a gesture. During a pinch or wheel zoom, scale keeps its value until the gesture ends; zoomGestureStart and zoomGestureEnd from useViewportEvent mark the gesture.

Thumbnails

Before: Syncfusion PDF Viewer

The ThumbnailView module adds a thumbnails panel, opened from the navigation toolbar on the left of the viewer. isThumbnailViewOpen opens it on load, the thumbnailView module opens and closes it, and thumbnailClick reports the clicked page:

;<>
  <button onClick={() => viewerRef.current?.thumbnailView.openThumbnailPane()}>
    Thumbnails
  </button>
  <PdfViewerComponent
    ref={viewerRef}
    resourceUrl={RESOURCE_URL}
    isThumbnailViewOpen={true}
    thumbnailClick={(args) => trackThumbnailClick(args.pageNumber)}
    style={{ height: '640px' }}
  >
    <Inject services={[Toolbar, Navigation, ThumbnailView]} />
  </PdfViewerComponent>
</>

After: @sdk-pdf/react

PageCanvas renders a page lazily at the canvas's CSS size; the panel, its open state, and its highlight of the current page are yours:

import { useState } from 'react'
import { PageCanvas, useCurrentPageId, usePages, useScrollToPage } from '@sdk-pdf/react'

function ThumbnailsSidebar() {
  const pages = usePages()
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()
  // Replaces isThumbnailViewOpen, openThumbnailPane(), and closeThumbnailPane().
  const [isOpen, setIsOpen] = useState(true)

  return (
    <aside>
      <button aria-expanded={isOpen} onClick={() => setIsOpen(!isOpen)}>
        Thumbnails
      </button>
      {isOpen && (
        <nav>
          {pages.map((page, index) => (
            <button
              key={page.id}
              aria-current={page.id === currentPageId ? 'page' : undefined}
              // Replaces thumbnailClick: run your own code before or after scrolling.
              onClick={() => scrollToPage(page.id)}
            >
              <PageCanvas
                pageId={page.id}
                className="thumbnail"
                role="img"
                aria-label={`Page ${index + 1}`}
              />
            </button>
          ))}
        </nav>
      )}
    </aside>
  )
}

What to know

  • Lazy and sized with CSS. An off-screen canvas does no work until it becomes visible, and the render resolution follows the displayed CSS size or an explicit scale. In a virtualized list, pass isVisible.
  • Annotations included. PageCanvas draws annotations, form widgets included, unless hideAnnotations is set.
  • Thumbnails that show session edits are coming soon. PageCanvas draws the page as loaded from the file; the main view shows the edits. Rotation is followed live.
  • Render only when ready. PageCanvas throws DocumentNotLoadedError when no document is loaded.

Page organizer

Before: Syncfusion PDF Viewer

The PageOrganizer module adds the Organize Pages dialog: users rotate, rearrange, insert, delete, and copy pages, import pages from another PDF, and then save over the document or save a copy. pageOrganizerSettings chooses the actions, pageOrganizer.openPageOrganizer() opens the dialog, and pageOrganizerSaveAs receives the result as Base64. extractPages returns selected pages as a new PDF:

const pageOrganizerSettings = {
  canDelete: true,
  canInsert: true,
  canRotate: true,
  canCopy: true,
  canRearrange: true,
  canImport: false,
}

function Viewer() {
  const viewerRef = useRef(null)

  return (
    <>
      <button onClick={() => viewerRef.current?.pageOrganizer.openPageOrganizer()}>
        Organize pages
      </button>
      {/* A new PDF with pages 1 and 2, as a Uint8Array. */}
      <button onClick={() => uploadExtract(viewerRef.current?.extractPages('1,2'))}>
        Extract
      </button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        pageOrganizerSettings={pageOrganizerSettings}
        pageOrganizerSaveAs={(args) => {
          // Upload the result instead of the default Save As download.
          args.cancel = true
          uploadBase64(args.fileName, args.downloadDocument)
        }}
        style={{ height: '640px' }}
      >
        <Inject services={[Toolbar, Navigation, ThumbnailView, PageOrganizer]} />
      </PdfViewerComponent>
    </>
  )
}

After: @sdk-pdf/react

Each page operation is a hook that returns a command taking page ids: usePageCreate, usePageDelete, usePageMove, useSetPageRotation, and usePageDuplicate. The organizer panel is yours; it offers only the actions you render:

import {
  PageCanvas,
  usePageCreate,
  usePageDelete,
  usePageDuplicate,
  usePageMove,
  usePages,
  useSetPageRotation,
} from '@sdk-pdf/react'

function OrganizePagesPanel() {
  const pages = usePages()
  const createPage = usePageCreate()
  const deletePages = usePageDelete()
  const duplicatePage = usePageDuplicate()
  const { moveBefore, moveAfter } = usePageMove()
  const setPageRotation = useSetPageRotation()

  return (
    <ol>
      {pages.map((page, index) => (
        <li key={page.id}>
          <PageCanvas
            pageId={page.id}
            className="organizer-thumbnail"
            role="img"
            aria-label={`Page ${index + 1}`}
          />
          {/* Rotation is absolute: add to the current angle. -90 is normalized to
             270. */}
          <button onClick={() => setPageRotation(page.id, page.rotation - 90)}>
            Rotate left
          </button>
          <button onClick={() => setPageRotation(page.id, page.rotation + 90)}>
            Rotate right
          </button>
          <button
            disabled={index === 0}
            onClick={() => moveBefore(page.id, pages[index - 1].id)}
          >
            Move up
          </button>
          <button
            disabled={index === pages.length - 1}
            onClick={() => moveAfter(page.id, pages[index + 1].id)}
          >
            Move down
          </button>
          {/* Copy pages: the duplicate goes right after the source. */}
          <button onClick={() => void duplicatePage(page.id)}>Copy</button>
          {/* A blank page the size of this one as displayed, inserted after it. */}
          <button onClick={() => createPage(page.width, page.height, page.id)}>
            Insert blank page
          </button>
          <button disabled={pages.length <= 1} onClick={() => deletePages(page.id)}>
            Delete
          </button>
        </li>
      ))}
    </ol>
  )
}

What to know

Organize Pages action@sdk-pdf/react
RotatesetPageRotation(pageIdOrIds, rotation) sets an absolute angle; values outside 0..360 are normalized, so -90 and 450 work, and angles that aren't quarter turns throw InvalidInputError
RearrangemoveBefore(ids, beforePageId?) (start when omitted) or moveAfter(ids, afterPageId?) (end when omitted); drag and drop is your UI
Insert blank pagescreatePage(width, height, insertAfterPageId?): after the given page, or appended; the size in user-space units, with no defaults
DeletedeletePages(pageIdOrIds)
CopyduplicatePage(sourcePageId, insertAfterPageId?) is asynchronous and inserts after the source by default
Select all, bulk actionsPass several ids in one call
Import a PDFNo equivalent
extractPagesprepareDocument({ pageIds }) from useDocumentSaving resolves with a new PDF of the listed pages, in the listed order
Save, Save AsChanges are part of the document; save with prepareDocument() or downloadDocument()
  • Undo. Deleting, moving, or rotating several pages in one call is a single undo step, and an undo can restore a deleted page with its content; separate edits within 500 ms merge into one step.
  • No page events. Page operations your code calls emit no events: run your follow-up where you call them, and watch useDocumentRevision for any change.
  • No dialog to open. openPageOrganizer(), isPageOrganizerOpen, and the zoom slider settings become your own panel state and thumbnail size.

Forms

Before: Syncfusion PDF Viewer

The FormFields module makes fields fillable. Field objects come from formFieldCollections or retrieveFormFields(), with name, type, value, and isChecked, and a changed field goes back through updateFormFieldsValue. With enableFormFieldsValidation, required fields are checked before printing and downloading:

function Viewer() {
  const viewerRef = useRef(null)

  function prefill() {
    const viewer = viewerRef.current
    const field = viewer
      .retrieveFormFields()
      .find((field) => field.name === 'customer.name')
    if (field) {
      field.value = 'Jane Doe'
      viewer.updateFormFieldsValue(field)
    }
  }

  return (
    <PdfViewerComponent
      ref={viewerRef}
      resourceUrl={RESOURCE_URL}
      documentLoad={prefill}
      formFieldFocusOut={(args) => autosave(args.fieldName, args.value)}
      enableFormFieldsValidation={true}
      validateFormFields={(args) => showMissingFields(args.nonFillableFields)}
      style={{ height: '100vh' }}
    >
      <Inject services={[Toolbar, Navigation, FormFields]} />
    </PdfViewerComponent>
  )
}

After: @sdk-pdf/react

Forms are widget-centric: you work with the widgets on the pages, and each value is shaped like the value of the matching HTML form control, as Working with forms explains. Users fill fields in Viewport with nothing to build when the content policy allows interacting with forms. For form UI outside the main view, useWidgets lists the widgets in reading order and useWidgetValue binds one widget's value like a useState backed by the document:

import {
  CheckboxWidget,
  TextWidget,
  useWidgets,
  useWidgetValue,
  Widget,
} from '@sdk-pdf/react'

// The counterpart of formFieldCollections, re-rendered as the widgets change.
function FormPanel({ savedValues }: { savedValues: Record<string, string> }) {
  const { widgets, isLoaded } = useWidgets()

  // The list is partial until every page containing widgets has been loaded.
  if (!isLoaded) return <p>Loading the form…</p>

  return (
    <form>
      {widgets.map((widget) => (
        <FormField
          key={widget.id}
          widget={widget}
          savedValue={savedValues[widget.fieldName]}
        />
      ))}
    </form>
  )
}

function FormField({ widget, savedValue }: { widget: Widget; savedValue?: string }) {
  switch (widget.widgetType) {
    case 'text':
      return <TextField widget={widget} savedValue={savedValue} />
    case 'checkbox':
      return <CheckboxField widget={widget} />
    default:
      return null
  }
}

function TextField({ widget, savedValue }: { widget: TextWidget; savedValue?: string }) {
  // Filling in the main view and undo update this value.
  const [value, setValue] = useWidgetValue(widget)

  return (
    <label>
      {widget.fieldName}
      <input value={value} onChange={(event) => setValue(event.currentTarget.value)} />
      {/* Replaces setting field.value and calling updateFormFieldsValue. */}
      {savedValue !== undefined && (
        <button type="button" onClick={() => setValue(savedValue)}>
          Fill in
        </button>
      )}
    </label>
  )
}

function CheckboxField({ widget }: { widget: CheckboxWidget }) {
  // isChecked becomes the boolean value.
  const [isChecked, setIsChecked] = useWidgetValue(widget)

  return (
    <label>
      <input
        type="checkbox"
        checked={isChecked}
        onChange={(event) => setIsChecked(event.currentTarget.checked)}
      />
      {widget.fieldName}
    </label>
  )
}

From fields to widgets

ConceptSyncfusion PDF Viewer@sdk-pdf/react
LookupformFieldCollections, retrieveFormFields()useWidgets() (re-renders), or await getWidgets() from useWidgetsGetter (one-time, loads every page with widgets first)
Shared valuesRadio buttons grouped by the same nameEvery widget with the same field name shows and synchronizes the same value, radio groups included
Valuesfield.value, field.isChecked, then updateFormFieldsValueuseWidgetValue(widget): string for text and choice, boolean for checkbox and radio, option indices for multiChoice
ClearingclearFormFields(), resetFormFields()Set the empty value per widget with useWidgetValue; no reset to defaults
FocusfocusFormField(name)scrollToBlock(widget.id) from useScrollToBlock scrolls to the widget
EventsformFieldFocusOut, formFieldClick, and the other field eventswidgetInput from useViewportEvent reports a value the user changed, and useFocusedWidgetId returns the widget being filled; useWidgetValue re-renders
ValidationisRequired, enableFormFieldsValidation, validateFormFieldsNo equivalent; validate in your own submit flow
Form dataimportFormFields(), exportFormFields() as JSON, FDF, or XFDFNo equivalent; prepareDocument() produces the filled PDF

What to know

  • Values are read and written per widget. useWidgetValue binds one widget per hook call, and there is no setter by field name: to prefill a form, render a bound component per widget, like TextField above.
  • Field kinds. The widget kinds are text, choice, multiChoice, checkbox, radio, pushButton, and signature. Syncfusion's password fields and initial fields have no matching kind.
  • Read-only forms. enableFormFields={false} maps to a policy where widgets is 'readonly', such as PDF_PRINTER, or 'hidden' to leave the widgets out of the view. To lock a submitted form instead, set isReadOnly on the widgets, as Form designer shows.
  • Date fields. A text widget's format exposes Acrobat date formats as FormatType.DateTime, recognized without running scripts; see FormatType.
  • Signatures. signature widgets carry a read-only isCryptoSigned; the SDK never signs.

Form designer

Before: Syncfusion PDF Viewer

The FormDesigner module adds the form designer toolbar and the formDesigner module. Fields are added at given bounds or placed by the user after setFormFieldMode, updated with settings objects, and deleted by object or id:

function Designer() {
  const viewerRef = useRef(null)

  function addNameField() {
    viewerRef.current?.formDesigner.addFormField('Textbox', {
      name: 'customer.name',
      pageNumber: 1,
      bounds: { X: 100, Y: 150, Width: 200, Height: 24 },
      maxLength: 40,
      isRequired: true,
    })
  }

  function lockForm() {
    const viewer = viewerRef.current
    for (const field of viewer.retrieveFormFields()) {
      viewer.formDesigner.updateFormField(field, { isReadOnly: true })
    }
  }

  function deleteField(name) {
    const viewer = viewerRef.current
    const field = viewer.retrieveFormFields().find((field) => field.name === name)
    if (field) viewer.formDesigner.deleteFormField(field)
  }

  return (
    <>
      <button onClick={addNameField}>Add name field</button>
      {/* The next click on a page places a text box. */}
      <button onClick={() => viewerRef.current?.formDesigner.setFormFieldMode('Textbox')}>
        Place text box
      </button>
      <button onClick={lockForm}>Lock form</button>
      <button onClick={() => deleteField('customer.name')}>Delete name field</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        designerMode={true}
        formFieldAdd={(args) => trackFieldAdded(args.pageIndex)}
        style={{ height: '100vh' }}
      >
        <Inject services={[Toolbar, Navigation, FormFields, FormDesigner]} />
      </PdfViewerComponent>
    </>
  )
}

After: @sdk-pdf/react

useWidgetsCreate creates widgets, useSetWidgetProps updates them by id, and useBlockDelete deletes them:

import {
  useBlockDelete,
  useSetWidgetProps,
  useWidgetsCreate,
  useWidgetsGetter,
} from '@sdk-pdf/react'

function useFormDesigner() {
  const createWidgets = useWidgetsCreate()
  const setWidgetProps = useSetWidgetProps()
  const deleteBlocks = useBlockDelete()
  const getWidgets = useWidgetsGetter()

  // Replaces formDesigner.addFormField('Textbox', { name, bounds, maxLength }).
  async function addNameField(pageId: string) {
    const [widget] = await createWidgets({
      pageId,
      widgetType: 'text',
      // User-space units: the origin is at the bottom-left corner of the page.
      x: 72,
      y: 700,
      width: 180,
      height: 24,
      fieldName: 'customer.name',
      maxLength: 40,
    })
    return widget
  }

  // Replaces updateFormField(field, { isReadOnly: true }) for every field, in one call.
  async function lockForm() {
    const widgets = await getWidgets()
    // excludeFromHistory keeps undo from unlocking the submitted form.
    // The props apply at once; the promise resolves when the widgets show them.
    await setWidgetProps(
      widgets.map((widget) => widget.id),
      { isReadOnly: true },
      { excludeFromHistory: true }
    )
  }

  // Replaces deleteFormField(field): same-named widgets act as one field, so delete them
  // together.
  async function deleteField(fieldName: string) {
    const widgets = await getWidgets()
    const widgetIds = widgets
      .filter((widget) => widget.fieldName === fieldName)
      .map((widget) => widget.id)
    if (widgetIds.length > 0) deleteBlocks(widgetIds)
  }

  return { addNameField, lockForm, deleteField }
}

To let users place a widget by clicking the page, like setFormFieldMode, switch the interaction tool to { type: 'addWidget', widgetProps } with useSetInteractionMode; pass a policy where widgets are editable, such as PDF_EDITOR, with the tool.

From field settings to widget props

Syncfusion field settings@sdk-pdf/react widget props
Textbox, CheckBox, RadioButtonwidgetType 'text', 'checkbox', 'radio'
DropDown, ListBox'choice' (with isCombo for a dropdown) or 'multiChoice'
SignatureField'signature'
Password, InitialFieldNo equivalent
namefieldName; widgets sharing it share the value
pageNumber and boundspageId and x, y, width, height in user-space units from the bottom-left corner
isReadOnly, maxLength, isMultilineisReadOnly, maxLength, isMultiline
backgroundColor, borderColor, thicknessfillColor, borderColor, borderWidth
color, fontSize, alignmenttextColor, fontSize, horizontalAlign
options with itemName and itemValueoptions with label and value
isRequired, tooltip, visibility, isPrint, fontFamilyNo equivalent

What to know

  • Convert stored positions. @sdk-pdf/react widget positions are user-space units, 1/72 of an inch, with the origin at the bottom-left corner of the page. Check placement on a test document before converting bounds you stored for Syncfusion.
  • Designer UI. The form designer toolbar and its properties panel become your own controls. useSelectBlocks replaces selectFormField and clearSelection (selectBlocks([])), useSelectedBlocks returns the selected widgets for a properties panel, setWidgetProps from useSetWidgetProps changes their props, such as isMultiline and format, useSelectionAnnotationProps changes their border color and width, and useDuplicateSelection copies them under new field names.
  • Replacing a kind. A widget's kind can't be changed through its props: create the new widget with the same field name and delete the old one, as Working with forms shows.
  • Undo. Creating widgets is an undo step, and so is changing their props, unless you pass { excludeFromHistory: true } to setWidgetProps.
  • Design and fill modes. designerMode and enableFormDesigner map to the content policy's widgets level: 'interactive' (as in PDF_VIEWER) lets users fill fields, and 'editable' (as in PDF_EDITOR) makes widgets editable, as the addWidget tool needs. The level can be a function of the widget id, such as widgets: (blockId) => (lockedIds.has(blockId) ? 'interactive' : 'editable'). isReadOnly restricts filling a field, not authoring the widget.

Editing and interaction tools

Before: Syncfusion PDF Viewer

interactionMode switches between text selection (the default) and panning, annotation.setAnnotationMode arms an annotation tool for the next interaction, and the enable* props remove the editing features for read-only users:

function Viewer({ isReadOnly }) {
  const viewerRef = useRef(null)

  return (
    <>
      <button onClick={() => (viewerRef.current.interactionMode = 'TextSelection')}>
        Select text
      </button>
      <button onClick={() => (viewerRef.current.interactionMode = 'Pan')}>Pan</button>
      <button onClick={() => viewerRef.current?.annotation.setAnnotationMode('FreeText')}>
        Free text
      </button>
      <button onClick={() => viewerRef.current?.annotation.setAnnotationMode('None')}>
        Stop
      </button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        interactionMode="TextSelection"
        enableAnnotation={!isReadOnly}
        enableFormDesigner={!isReadOnly}
        enablePageOrganizer={!isReadOnly}
        style={{ height: '100vh' }}
      >
        <Inject
          services={[
            Toolbar,
            Navigation,
            TextSelection,
            Annotation,
            FormFields,
            FormDesigner,
            PageOrganizer,
          ]}
        />
      </PdfViewerComponent>
    </>
  )
}

After: @sdk-pdf/react

The interaction mode combines a content policy (what may change) and an interaction tool (what a click, tap, or drag does); useSetInteractionMode switches both at once, and useInteractionTool reads the tool back:

import {
  CONTENT_POLICY_PRESET,
  InteractionTool,
  useInteractionTool,
  useSetInteractionMode,
} from '@sdk-pdf/react'

const textSelectTool: InteractionTool = { type: 'textSelect' }
const selectTool: InteractionTool = { type: 'select' }
const addTextTool: InteractionTool = { type: 'addText' }

function ModeSwitch() {
  const interactionTool = useInteractionTool()
  const setInteractionMode = useSetInteractionMode()

  return (
    <div role="toolbar">
      {/* Like interactionMode="TextSelection" with the editing modules off. */}
      <button
        aria-pressed={interactionTool.type === 'textSelect'}
        onClick={() =>
          setInteractionMode(textSelectTool, CONTENT_POLICY_PRESET.PDF_VIEWER)
        }
      >
        Read
      </button>
      {/* Select, move, and resize blocks; page text is editable in place. */}
      <button
        aria-pressed={interactionTool.type === 'select'}
        onClick={() => setInteractionMode(selectTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Edit
      </button>
      {/* The nearest tool to free text: a new paragraph at the click position. */}
      <button
        aria-pressed={interactionTool.type === 'addText'}
        onClick={() => setInteractionMode(addTextTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Add text
      </button>
    </div>
  )
}

Mapping the modes

Syncfusion PDF Viewer@sdk-pdf/react
interactionMode="TextSelection"{ type: 'textSelect' }: native text selection for reading and copying, with read-only text.
interactionMode="Pan"No tool: Viewport scrolls and zooms on its own.
setAnnotationMode('None')Back to { type: 'select' } or { type: 'textSelect' }.
setAnnotationMode('FreeText')The nearest tool is { type: 'addText' }, which creates a text paragraph in editable text.
Image stamps{ type: 'addImage', image, width?, height? } with await stageImage(blob) places a stamp annotation. See Annotations.
formDesigner.setFormFieldMode(type){ type: 'addWidget', widgetProps } with editable widgets.
InkNo drawing tool. useInkAnnotationCreate creates an ink annotation from strokes your code collects.
Text markup, shapes, measurement, StickyNotes, HandWrittenSignature, Initial, RedactionNo equivalent.
enableAnnotation, enableFormDesigner, enableFormFields, designerModeThe annotations and widgets levels of the content policy: PDF_EDITOR makes everything editable, PDF_VIEWER lets users fill forms, PDF_PRINTER makes everything read-only but links.

What to know

  • Switch the policy with the tool. Pass PDF_EDITOR with the editing tools, as above. textSelect needs read-only text: with editable text, the combination is unsupported and reports a message through logMessages.
  • Per-kind permissions. A custom policy is a plain object with text, annotations, links, and widgets levels, finer than turning modules on and off; useSetInteractionMode lists them. Each level can also be 'hidden', which leaves that content out of the view, or a function of the block id for per-block levels. There is no hook to read the policy back: keep the policy you set in your own state to show or hide editing UI.
  • Editing existing text is new. Under PDF_EDITOR, page text is editable in place and Viewport handles typing. useSelectionTextStyle drives a style toolbar (font, size, color, bold, italic, alignment, spacing), and useFocusSelection returns the focus after a toolbar click.
  • Local mode. Switching the mode affects this client only.
  • Keyboard shortcuts. Wire the shortcuts you need, such as delete with useDeleteSelection or arrow-key nudging with useMoveSelection, through useHotkeys with a memoized mapping. Wrap block shortcuts in createUnfocusedHandler, so they stay inactive while the user types in a paragraph or a form field.

Annotations

Before: Syncfusion PDF Viewer

The Annotation module adds the annotation toolbar and the annotation module: text markup, shapes, measurements, free text, ink, stamps, and sticky notes, with comments, default settings such as the author, and JSON or XFDF export:

function ReviewTools() {
  const viewerRef = useRef(null)

  function addHighlight() {
    viewerRef.current?.annotation.addAnnotation('Highlight', {
      bounds: [{ x: 97, y: 110, width: 350, height: 14 }],
      pageNumber: 1,
    })
  }

  function recolorMine() {
    const viewer = viewerRef.current
    for (const annotation of viewer.annotationCollection) {
      if (annotation.author === 'Reviewer') {
        annotation.color = '#ff0000'
        viewer.annotation.editAnnotation(annotation)
      }
    }
  }

  async function storeAnnotations() {
    // Annotations are stored apart from the PDF.
    await saveAnnotations(await viewerRef.current.exportAnnotationsAsObject())
  }

  return (
    <>
      <button onClick={addHighlight}>Highlight</button>
      <button onClick={recolorMine}>Recolor mine</button>
      <button onClick={storeAnnotations}>Store annotations</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        annotationSettings={{ author: 'Reviewer' }}
        annotationAdd={(args) => trackAnnotation(args.annotationType, args.pageIndex)}
        style={{ height: '100vh' }}
      >
        <Inject services={[Toolbar, Navigation, TextSelection, Annotation]} />
      </PdfViewerComponent>
    </>
  )
}

After: @sdk-pdf/react

Existing annotations display in Viewport and in PageCanvas, and selected shapes, lines, ink, and widgets can be restyled with useSelectionAnnotationProps. Images become stamp annotations, placed with the addImage tool, which takes an image staged with stageImage:

import {
  CONTENT_POLICY_PRESET,
  stageImage,
  useSelectionAnnotationProps,
  useSetInteractionMode,
} from '@sdk-pdf/react'

function AnnotationToolbar() {
  const { isApplicable, borderColor, borderWidth } = useSelectionAnnotationProps()
  const setInteractionMode = useSetInteractionMode()

  async function placeImage(file: File) {
    // Replaces an image stamp: the next click on a page places the image as a stamp
    // annotation.
    const image = await stageImage(file)
    // Placing images is editing, so set the policy with the tool. The width is in
    // user-space units.
    setInteractionMode(
      { type: 'addImage', image, width: 150 },
      CONTENT_POLICY_PRESET.PDF_EDITOR
    )
  }

  return (
    <div role="toolbar">
      <input
        type="file"
        accept="image/*"
        onChange={(event) => {
          const file = event.currentTarget.files?.[0]
          if (file) placeImage(file).catch(showError)
        }}
      />
      {/* Like the stroke color and thickness pickers, for the selected annotations. */}
      {isApplicable && (
        <>
          <input
            type="color"
            value={borderColor.value ?? '#000000'}
            onChange={(event) => borderColor.set(event.currentTarget.value)}
          />
          <input
            type="number"
            min={0}
            value={borderWidth.value ?? ''}
            onChange={(event) => borderWidth.set(Number(event.currentTarget.value))}
          />
        </>
      )}
    </div>
  )
}

What to know

  • Few annotation kinds. Creating text markup, shapes, measurements, free text, stamps other than images, sticky notes, handwritten signatures, and redactions has no equivalent, from the UI or from code (addAnnotation). From code, useStampAnnotationCreate places an image stamp at a position on a page, and useInkAnnotationCreate creates ink from strokes your code collects; there is no ink drawing tool.
  • Restyling is the border. useSelectionAnnotationProps changes the border color, with the opacity in its alpha channel, and the border width of the selected shapes, lines, ink, and widgets, and skips the rest of the selection. editAnnotation with other properties has no equivalent.
  • Images. stageImage(blob) accepts any image format the browser decodes, and SVGs are rasterized when the PDF is saved. The size goes on the tool or on useStampAnnotationCreate: width and height in user-space units, a single dimension keeping the aspect ratio. Without a size, the image's natural size is used, fit down to the page.
  • No annotation data. annotationCollection, the annotation events, the comments panel, annotationSettings (author, locking, skipPrint, skipDownload), and JSON or XFDF import and export have no equivalent. useDocumentRevision changes on any edit, and addToolApplied from useViewportEvent reports a stamp the user placed.
  • Hiding annotations in previews. PageCanvas draws annotations unless hideAnnotations is set.

Undo, redo, and change tracking

Before: Syncfusion PDF Viewer

undo() and redo() revert and reapply annotation edits, from the toolbar's undo and redo tools, the keyboard, or code. isDocumentEdited tells whether the document was edited, such as for a warning before leaving the page:

function Viewer() {
  const viewerRef = useRef(null)

  useEffect(() => {
    function warnIfEdited(event) {
      if (viewerRef.current?.isDocumentEdited) event.preventDefault()
    }
    window.addEventListener('beforeunload', warnIfEdited)
    return () => window.removeEventListener('beforeunload', warnIfEdited)
  }, [])

  return (
    <>
      <button onClick={() => viewerRef.current?.undo()}>Undo</button>
      <button onClick={() => viewerRef.current?.redo()}>Redo</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        style={{ height: '100vh' }}
      >
        <Inject services={[Toolbar, Navigation, Annotation, FormFields]} />
      </PdfViewerComponent>
    </>
  )
}

After: @sdk-pdf/react

useUndoRedo returns the commands and their availability, and useHotkeys wires the shortcuts:

import { useMemo } from 'react'
import { useHotkeys, useUndoRedo, useViewportEvent } from '@sdk-pdf/react'

function HistoryControls() {
  // canUndo and canRedo re-render the component as the history changes.
  const { undo, redo, canUndo, canRedo } = useUndoRedo()

  // The mapping must be memoized, or the listener re-installs on every render.
  useHotkeys(useMemo(() => ({ 'Mod-z': undo, 'Shift-Mod-z': redo }), [undo, redo]))

  // Events report what the user does in the viewport, such as moving blocks by dragging.
  useViewportEvent('blocksDragEnd', ({ blockIds }) => trackMove(blockIds.length))

  return (
    <div role="toolbar">
      <button disabled={!canUndo} onClick={undo}>
        Undo
      </button>
      <button disabled={!canRedo} onClick={redo}>
        Redo
      </button>
    </div>
  )
}

isDocumentEdited becomes document revisions: useDocumentRevision re-renders with a new revision on every edit, and undo and redo bring back the revision of that point in the history. Keep the revision you last saved and compare: a different revision means unsaved changes. A save records the revision it saved, so the unsaved-changes indicator and the leave-page warning live with the save button in Saving and downloading.

What to know

  • One history for the whole document, not only for annotations: deleting, moving, or rotating pages are undo steps, widget property changes are undo steps unless excluded, page text edits are undo steps, and form values follow undo and redo.
  • Local and merged. Undo reverts the local user's changes, and edits within 500 ms merge into one step, even across hook calls. To keep an operation a separate step, call markUndoBoundary() from useUndoBoundary before it starts and after its edits finish.
  • Shortcuts. Mod is Cmd on macOS and Ctrl elsewhere; a handler that returns false lets the event reach the browser, and a falsy value instead of the mapping disables the shortcuts, such as while a modal is open.
  • Events for user actions only. useViewportEvent reports what users do in the main view, such as textEditorInput, addToolApplied, blocksDragEnd, blockResizeDragEnd, and widgetInput. Page operations, undo, and redo that your code calls emit no events: track them where you call them, and watch useDocumentRevision for any change.

Saving and downloading

Before: Syncfusion PDF Viewer

saveAsBlob() resolves with the edited PDF as a Blob, with annotations, form values, and page changes applied, and download() saves it on the user's device under downloadFileName. downloadStart can cancel a download, and annotationSettings.skipDownload leaves annotations out. Server-backed integrations can save on the web service in its Download action instead:

function Viewer() {
  const viewerRef = useRef(null)

  async function upload() {
    const blob = await viewerRef.current.saveAsBlob()
    const body = new FormData()
    body.append('file', blob, 'contract.pdf')
    await fetch('/api/documents', { method: 'POST', body })
  }

  return (
    <>
      <button onClick={upload}>Save</button>
      <button onClick={() => viewerRef.current?.download()}>Download</button>
      <PdfViewerComponent
        ref={viewerRef}
        resourceUrl={RESOURCE_URL}
        downloadStart={(args) => {
          if (!canDownload()) args.cancel = true
        }}
        style={{ height: '100vh' }}
      >
        <Inject services={[Toolbar, Navigation, Annotation, FormFields]} />
      </PdfViewerComponent>
    </>
  )
}

After: @sdk-pdf/react

useDocumentSaving applies the recorded changes to the original document. The component that saves also owns the saved revision, so the save, the indicator, and the leave-page warning compare against the same value. Render it only once the document is ready, so its first revision is the loaded document's:

import { useEffect, useRef, useState } from 'react'
import {
  useDocumentRevision,
  useDocumentRevisionGetter,
  useDocumentSaving,
} from '@sdk-pdf/react'

function SaveControls() {
  const revision = useDocumentRevision()
  const getRevision = useDocumentRevisionGetter()
  // The revision on the server: the loaded document's revision until the first save.
  const [savedRevision, setSavedRevision] = useState(revision)
  // One save at a time, so an overlapping save can't mark a failed upload as saved.
  const isSavingRef = useRef(false)
  const { prepareDocument, downloadDocument } = useDocumentSaving()

  // Replaces isDocumentEdited: undoing back to the saved state clears it too.
  const hasUnsavedChanges = revision !== savedRevision

  // Warn before leaving the page with unsaved changes.
  useEffect(() => {
    if (!hasUnsavedChanges) return
    const warn = (event: BeforeUnloadEvent) => event.preventDefault()
    window.addEventListener('beforeunload', warn)
    return () => window.removeEventListener('beforeunload', warn)
  }, [hasUnsavedChanges])

  async function upload() {
    if (isSavingRef.current) return
    isSavingRef.current = true
    try {
      // Edits made after the save starts may be missing from the saved bytes, so only
      // this revision becomes saved.
      const startRevision = getRevision()
      // Replaces saveAsBlob(). The bytes go to fetch, so don't hand out the loaded
      // document's buffer.
      const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })

      const body = new FormData()
      body.append(
        'file',
        new Blob([bytes as BlobPart], { type: 'application/pdf' }),
        'contract.pdf'
      )
      const response = await fetch('/api/documents', { method: 'POST', body })
      // A failed upload keeps the unsaved-changes indicator.
      if (!response.ok) return

      setSavedRevision(startRevision)
    } finally {
      isSavingRef.current = false
    }
  }

  return (
    <div role="toolbar">
      <button onClick={() => void upload()}>Save</button>
      <button onClick={() => void downloadDocument()}>Download</button>
      {hasUnsavedChanges && <span>Unsaved changes</span>}
    </div>
  )
}

What to know

  • One artifact. Where Syncfusion lets you store annotations and form data apart from the PDF (JSON, XFDF, or FDF), @sdk-pdf/react saves the complete PDF: prepareDocument() resolves with its bytes as a Uint8Array, and downloadDocument() saves the same bytes on the user's device.
  • A Uint8Array, not a Blob. Wrap the bytes in a Blob for FormData, as above, or send them as the request body.
  • returnOriginalIfUnchanged defaults to true, returning the buffer passed to loadPdf when nothing changed; set it to false when the bytes will be transferred or mutated. The other option, pageIds, saves only the listed pages, as extractPages does. There is no download file name, flattening, or option to leave annotations out.
  • No download events. Instead of cancelling in downloadStart or reacting in downloadEnd, run your checks before calling downloadDocument() and your follow-up after awaiting it. Both functions throw DocumentNotLoadedError when no document is loaded.
  • Server-backed saving. The web service's Download action has no counterpart: post the bytes from prepareDocument() to your own endpoint, which stores them as it did before.

Collaboration

Syncfusion's collaborative editing connects the viewer to a Collaboration Server through @syncfusion/ej2-collaborator and a PdfViewerAdapter, with Redis behind the server, and shares annotations, form field changes, and Organize Pages operations between users.

Real-time collaboration in @sdk-pdf/react is coming soon. The adapter and the Collaboration Server setup don't carry over.

Errors and logging

Before: Syncfusion PDF Viewer

Load failures arrive through documentLoadFailed, and the viewer shows errors in its own message box, which showNotificationDialog={false} turns off; showNotificationPopup(message) shows a message in it:

<PdfViewerComponent
  ref={viewerRef}
  resourceUrl={RESOURCE_URL}
  // Show your own error UI instead of the built-in message box.
  showNotificationDialog={false}
  documentLoadFailed={(args) => showError(`Couldn't open ${args.documentName}`)}
  style={{ height: '100vh' }}
>
  <Inject services={[Toolbar, Navigation]} />
</PdfViewerComponent>

After: @sdk-pdf/react

Failures are typed errors from the call that failed, and recoverable problems are log messages you subscribe to with logMessages. Error handling and logging has the full rules:

import { useEffect } from 'react'
import { InvalidPasswordError, logMessages, LogLevel, useLoadPdf } from '@sdk-pdf/react'

function useReportSdkWarnings() {
  useEffect(() => {
    // The SDK never prints to the console: subscribe, and unsubscribe on unmount.
    const unsubscribe = logMessages.on('message', (logMessage) => {
      if (logMessage.level >= LogLevel.Warning) {
        reportToMonitoring(logMessage.message, logMessage.data)
      }
    })
    return () => {
      unsubscribe()
    }
  }, [])
}

function useOpenBytes() {
  const [, loadPdf] = useLoadPdf()

  return async (data: Uint8Array) => {
    try {
      // Pass a copy and keep `data` for a password retry.
      await loadPdf(data.slice())
    } catch (error) {
      // Replaces documentLoadFailed for this load.
      if (error instanceof InvalidPasswordError) askForPassword()
      else showError(error)
    }
  }
}

What to know

  • Error classes. DocumentNotLoadedError, InvalidInputError, InvalidPasswordError, DocumentFormatError (malformed beyond what the SDK can work around), NotSupportedError (a PDF feature not supported yet), and UninitializedError, all extending GenericError with a static message and an optional data object. Narrow them with instanceof.
  • Nothing half-applied. Commands validate first and throw before touching anything; readers return empty values instead of throwing; hooks never throw during render. Viewport and PageCanvas are the exception: they throw DocumentNotLoadedError when rendered without a document.
  • No dialogs. The SDK shows no message box, so showNotificationDialog and showNotificationPopup have no counterpart: error UI is yours.
  • Logs are opt-in. Each message carries level (LogLevel.Debug, Info, Warning, Error), reason, a static message, data, and stack. Filter by level in your subscriber.

Cleanup and lifecycle

Before: Syncfusion PDF Viewer

unload() closes the document and fires documentUnload, and destroy() releases the instance's resources. By default the viewer keeps session data, such as form fields, annotations, and signatures, in the browser's sessionStorage; with enableLocalStorage it keeps them in memory, and the instance should be destroyed when it's no longer needed:

<PdfViewerComponent
  ref={viewerRef}
  resourceUrl={RESOURCE_URL}
  enableLocalStorage={true}
  documentUnload={() => clearDocumentState()}
  style={{ height: '100vh' }}
>
  <Inject services={[Toolbar, Navigation]} />
</PdfViewerComponent>

After: @sdk-pdf/react

Concern@sdk-pdf/react
Closing a documentresetPdf() from useResetPdf unloads the document and invalidates its ids, snapshots, and revisions; run your documentUnload code after calling it.
Event listenersuseViewportEvent subscribes for the component's lifetime, with a callback that doesn't need memoizing.
Keyboard listenersuseHotkeys installs window-level shortcuts; pass a falsy value instead of the mapping to disable them.
Log subscriptionslogMessages.on() returns an unsubscribe function: call it in the effect cleanup, as in Errors and logging.
BuffersDon't reuse a buffer passed to loadPdf; copy it first.

There is no destroy() to call and no storage setting: the document lives in PdfProviders in your React tree.

The Syncfusion viewer extracts text with extractText(pageIndex, options), which resolves with textData and pageText, and searches with the TextSearch module (searchText, findText). In @sdk-pdf/react, getParagraphsByPage(pageId) from useProgrammaticEditor resolves with paragraph entries in content order: id, content, pageNumber, the box bounds ({ x, y, width, height }), and the paragraph layout, such as textAlign and lineHeight:

import { usePages, useProgrammaticEditor } from '@sdk-pdf/react'

function useExtractPageText() {
  const pages = usePages()
  const { getParagraphsByPage } = useProgrammaticEditor()

  // Replaces extractText(pageIndex, ExtractTextOption.TextOnly): the index is zero-based
  // in both.
  return async (pageIndex: number) => {
    const page = pages[pageIndex]
    if (!page) return ''

    const paragraphs = await getParagraphsByPage(page.id)
    return paragraphs.map((paragraph) => paragraph.content).join('\n\n')
  }
}

The bounds are user-space units with the origin at the bottom-left corner of the page.

useDocumentSearch replaces the TextSearch module: set the query and the options, and the SDK finds the matches, highlights them on the pages, and scrolls to the active one:

import { useDocumentSearch } from '@sdk-pdf/react'

function SearchBar() {
  const {
    criteria,
    matches,
    activeMatchIndex,
    status,
    setSearchCriteria,
    goToNextMatch,
    goToPreviousMatch,
  } = useDocumentSearch()

  return (
    <div role="search">
      {/* Replaces searchText(text, isMatchCase); an empty query replaces
         cancelTextSearch(). */}
      <input
        value={criteria.query}
        onChange={(event) =>
          setSearchCriteria({ ...criteria, query: event.currentTarget.value })
        }
      />
      <label>
        <input
          type="checkbox"
          checked={criteria.isCaseSensitive}
          onChange={(event) =>
            setSearchCriteria({
              ...criteria,
              isCaseSensitive: event.currentTarget.checked,
            })
          }
        />
        Match case
      </label>
      <span>
        {status === 'searching'
          ? 'Searching…'
          : `${activeMatchIndex + 1} of ${matches.length}`}
      </span>
      {/* Replace searchPrevious() and searchNext(). */}
      <button disabled={matches.length === 0} onClick={goToPreviousMatch}>
        Previous
      </button>
      <button disabled={matches.length === 0} onClick={goToNextMatch}>
        Next
      </button>
    </div>
  )
}
  • Options. isCaseSensitive replaces isMatchCase, and isWholeWord matches whole words only.
  • Matches. Each match carries id, pageId, paragraphId, and the matched text, in document order; goToMatch(match.id) activates one from a results list. A changed query is searched once typing pauses, and status turns 'complete' once the whole document is searched, in place of textSearchComplete. Matches carry no bounds, so findText has no counterpart.
  • No selection API. There is no API to read the user's text selection. To jump to a paragraph you located yourself, call scrollToBlock(paragraphId) from useScrollToBlock.

Features without an equivalent

Plan around these before you migrate:

Syncfusion PDF Viewer featureIn @sdk-pdf/react
Server-backed mode (serviceUrl and the web service)No equivalent; the SDK runs in the browser, and your server keeps serving and storing the bytes
Printing (Print)No print API; PageCanvas can render pages for your own print layout, but it draws the pages as loaded from the file, without the session's edits
Bookmarks (BookmarkView), hyperlink eventsNo equivalent
Text markup, shape, measurement, free text, and sticky note annotations; ink drawing; the comments panelNo equivalent; existing shapes, lines, and ink display, their border color and width can be changed, and ink can be created from code with useInkAnnotationCreate
Dynamic, sign here, standard business, and text stampsNo equivalent; images are placed with the addImage tool
Handwritten signatures, initials, digital signatures, redactionNo equivalent; the SDK never signs
Annotation and form data import and export (JSON, XFDF, FDF), flattening, skipDownloadNo equivalent
Resetting form fields to their default values (resetFormFields)No equivalent; set each widget's value with useWidgetValue
Form validation (isRequired, validateFormFields), password fields, initial fields, field tooltipsNo equivalent
Importing pages from another PDFNo equivalent
Search hit bounds (findText)No equivalent; useDocumentSearch highlights the matches itself
Coordinate conversion (convertClientPointToPagePoint, convertPagePointToClientPoint)No equivalent
Fit to page on load (zoomMode="FitToPage")Fits are one-time calculations; see Zoom and fit modes
Download events and file name (downloadStart, downloadEnd, downloadFileName)No equivalent
Built-in toolbars, context menu, dialogs, theme packages, localization (locale)The UI is yours

On this page