Skip to main content
Add text comments in an editor The Nutrient integration renders each Velt comment as a view-only overlay positioned over the commented PDF text with Nutrient CustomOverlayItems. It does not modify your PDF. Comment anchors are stored as a durable { text, pageNumber, occurrence } object and re-resolved against the page text whenever comments render.

Setup

Step 1: Add Comment components

  • Add the Velt Comments component to the root of your app. This component is required to create and render comments in your app.
  • Authenticate the user with authProvider and set the Velt document before users add comments.
  • Set the textMode prop to false to hide the default Velt text comment tool. Nutrient selections are handled by @veltdev/nutrient-velt-comments.
  • Add VeltCommentsSidebar if you want a Google Docs-style comment sidebar.
import { VeltComments, VeltCommentsSidebar, VeltProvider, useSetDocument } from '@veltdev/react';

const user = {
  userId: 'user-1',
  organizationId: 'org-1',
  name: 'User One',
  email: 'user@example.com',
};

function VeltSetup() {
  useSetDocument('document-id', { documentName: 'Document name' });
  return null;
}

function App() {
  return (
    <VeltProvider apiKey="API_KEY" authProvider={{ user }}>
      <VeltSetup />
      <VeltComments textMode={false} />
      <VeltCommentsSidebar />
      {/* Your app content */}
    </VeltProvider>
  );
}

Step 2: Install the Velt Nutrient extension

npm i @veltdev/nutrient-velt-comments @nutrient-sdk/viewer@1.15.1
@nutrient-sdk/viewer is a peer dependency used for types. Use the same Nutrient version that you load from the CDN script. Load the Nutrient Web SDK at runtime so your app does not bundle the browser-only PDF viewer and WebAssembly assets.
import Script from 'next/script';

const NUTRIENT_CDN_SCRIPT =
  'https://cdn.cloud.nutrient.io/pspdfkit-web@1.15.1/nutrient-viewer.js';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Script src={NUTRIENT_CDN_SCRIPT} strategy="afterInteractive" />
        {children}
      </body>
    </html>
  );
}
If you self-host Nutrient assets, copy node_modules/@nutrient-sdk/viewer/dist/ into your static folder and load the SDK script from that path. The Velt Nutrient package expects the runtime SDK namespace on window.NutrientViewer.

Step 3: Configure the Nutrient viewer with the Velt Comments extension

Load the Nutrient viewer, then attach NutrientVeltComments to the viewer instance after NutrientViewer.load(...) resolves. The same instance is used by addComment, captureSelection, and renderComments.
import { useEffect, useRef, useState } from 'react';
import { useCommentAnnotations } from '@veltdev/react';
import {
  NutrientVeltComments,
  renderComments,
  type AttachedExtension,
  type NutrientInstance,
} from '@veltdev/nutrient-velt-comments';

type NutrientViewerApi = (typeof import('@nutrient-sdk/viewer'))['default'];

declare global {
  interface Window {
    NutrientViewer?: NutrientViewerApi;
  }
}

const EDITOR_ID = 'my-pdf';

function waitForNutrientViewer(timeoutMs = 30000): Promise<NutrientViewerApi> {
  return new Promise((resolve, reject) => {
    const start = Date.now();

    const tick = () => {
      if (window.NutrientViewer) {
        resolve(window.NutrientViewer);
        return;
      }

      if (Date.now() - start > timeoutMs) {
        reject(new Error('Nutrient SDK did not load.'));
        return;
      }

      window.setTimeout(tick, 50);
    };

    tick();
  });
}

function NutrientViewerComponent() {
  const viewerRef = useRef<HTMLDivElement | null>(null);
  const instanceRef = useRef<NutrientInstance | null>(null);
  const extensionRef = useRef<AttachedExtension | null>(null);
  const [instance, setInstance] = useState<NutrientInstance | null>(null);
  const annotations = useCommentAnnotations();

  useEffect(() => {
    if (!viewerRef.current || instanceRef.current) return;

    let cancelled = false;
    const container = viewerRef.current;

    waitForNutrientViewer().then((Nutrient) => {
      if (cancelled || instanceRef.current) return;

      Nutrient.unload(container);

      Nutrient.load({
        container,
        document: '/your-document.pdf',
        licenseKey: 'YOUR_NUTRIENT_LICENSE_KEY',
      }).then((nutrientInstance) => {
        if (cancelled) {
          Nutrient.unload(container);
          return;
        }

        instanceRef.current = nutrientInstance;
        extensionRef.current = NutrientVeltComments
          .configure({ editorId: EDITOR_ID })
          .attach(nutrientInstance);
        setInstance(nutrientInstance);
      });
    });

    return () => {
      cancelled = true;
      extensionRef.current?.detach();
      extensionRef.current = null;
      window.NutrientViewer?.unload(container);
      instanceRef.current = null;
      setInstance(null);
    };
  }, []);

  useEffect(() => {
    if (!instance) return;

    renderComments({
      instance,
      commentAnnotations: annotations ?? [],
    });
  }, [instance, annotations]);

  return <div ref={viewerRef} style={{ width: '100%', height: '100vh' }} />;
}

Step 4: Add a comment button to your Nutrient viewer

Add a button that users can click after selecting text in the PDF. Call captureSelection(instance) before addComment({ instance }) so the Velt Nutrient package records the current Nutrient selection before focus moves to your host UI.
import { addComment, captureSelection } from '@veltdev/nutrient-velt-comments';

const handleAddComment = async () => {
  if (!instance) return;

  await captureSelection(instance);
  const result = await addComment({ instance });

  if (!result) {
    console.warn('Select text in the PDF before adding a comment.');
  }
};

<button
  onMouseDown={(event) => event.preventDefault()}
  onClick={handleAddComment}
>
  Add Comment
</button>

Step 5: Call addComment to add a comment

  • Call this method to add a comment to selected text in the Nutrient viewer.
  • Params: AddCommentArgs. It has the following properties:
    • instance: Nutrient viewer instance returned by NutrientViewer.load(...).
  • Returns: Promise<AddCommentResult | null>. It returns null if no text is selected or Velt is not loaded.
import { addComment, captureSelection } from '@veltdev/nutrient-velt-comments';

await captureSelection(instance);
const result = await addComment({ instance });
To scope comments to a specific viewer on pages with multiple Nutrient instances, pass an editor id when attaching the extension:
NutrientVeltComments.configure({ editorId: 'EDITOR_ID' }).attach(instance);

Step 6: Render comments in Nutrient viewer

  • Get comment data from the Velt SDK and render it in the Nutrient viewer.
  • Params: RenderCommentsArgs. It has the following properties:
    • instance: Nutrient viewer instance.
    • commentAnnotations: Array of Comment Annotation objects.
import { useEffect } from 'react';
import { useCommentAnnotations } from '@veltdev/react';
import { renderComments } from '@veltdev/nutrient-velt-comments';

const commentAnnotations = useCommentAnnotations();

useEffect(() => {
  if (!instance) return;

  renderComments({
    instance,
    commentAnnotations: commentAnnotations ?? [],
  });
}, [instance, commentAnnotations]);
Highlights are view-only. The library re-derives each highlight from its TextEditorConfig anchor and Nutrient repositions the overlays across scroll, zoom, and page changes. Re-run renderComments when Velt’s annotation list changes or when you unload and reload the document.

Step 7: Clean up the Velt Nutrient extension

NutrientVeltComments.configure(...).attach(instance) returns an AttachedExtension. Call detach() in your effect cleanup to remove listeners, clear per-instance state, and remove overlay elements before unloading the viewer.
return () => {
  extensionRef.current?.detach();
  extensionRef.current = null;
  window.NutrientViewer?.unload(container);
};

Step 8: Style the commented text

  • Each highlight is a div.velt-nutrient-highlight inside a velt-comment-text overlay.
  • Override the default inline highlight styles with !important.
velt-comment-text .velt-nutrient-highlight {
  background-color: rgba(255, 212, 0, 0.4) !important;
  border-bottom: 2px solid rgba(255, 170, 0, 0.95) !important;
}

velt-comment-text:hover .velt-nutrient-highlight,
velt-comment-text[comment-selected="true"] .velt-nutrient-highlight,
velt-comment-text.velt-comment-selected .velt-nutrient-highlight {
  background-color: rgba(255, 212, 0, 0.7) !important;
}

Complete Example

APIs

NutrientVeltComments

Creates the Velt Comments extension for a Nutrient viewer. Use NutrientVeltComments.configure(...).attach(instance) to attach it to a viewer instance.
import { NutrientVeltComments } from '@veltdev/nutrient-velt-comments';

const attached = NutrientVeltComments
  .configure({ editorId: 'my-pdf' })
  .attach(instance);

attached.detach();

captureSelection()

Records the current Nutrient text selection for the next addComment call.
  • Signature: (instance: NutrientInstance) => Promise<void>
  • Params: instance: NutrientInstance
  • Returns: Promise<void>
import { captureSelection } from '@veltdev/nutrient-velt-comments';

await captureSelection(instance);

addComment()

Creates a comment annotation for the currently captured Nutrient text selection.
  • Signature: async (args: AddCommentArgs) => Promise<AddCommentResult | null>
  • Params: args: AddCommentArgs
  • Returns: Promise<AddCommentResult | null>; see AddCommentResult.
import { addComment } from '@veltdev/nutrient-velt-comments';

const result = await addComment({ instance });

renderComments()

Renders and updates Velt comment highlights in the Nutrient viewer.
import { renderComments } from '@veltdev/nutrient-velt-comments';

renderComments({
  instance,
  commentAnnotations: annotations ?? [],
});