Last date modified: 2026-Jul-14

Adding Custom Content

Extensions can add custom UI and behavior to the Review Interface in four main ways:


Cards

Cards are panels that appear in various docks around the document viewer — sidebars, top/bottom docks, and accordion panels.

For a full reference on card placement options, see Card Locations. For the complete card API, see Cards.


Registering cards

Cards are registered by adding one or more ICardConfig objects to the cards array on the extension config object returned from your extension script:

Copy
import type { IExtensionParameters, IExtensionConfig } from 'reviewapi';

export default function(parameters: IExtensionParameters): IExtensionConfig {
  return {
    id: 'acme.extension',
    name: 'ACME Extension',
    cards: [
      {
        id: 'acme-sidebar-card',
        title: 'ACME Panel',
        singleton: true,
        location: {
          layoutId: 'review',
          paneId: 'ri-review-right-accordion',
          dockIndex: 0,
        },
        loader: {
          custom: {
            loadCard: async (card, target) => { /* ... */ },
            unloadCard: async (card, target) => { /* ... */ },
          },
        },
      },
    ],
  };
}

Card loaders

The loader property on a card config accepts one of three loading strategies.

ICustomCardLoader is the recommended approach for CDN-hosted extensions. You provide two callbacks:

  • loadCard(card, target) — called when the card is opened. Render your content into target.
  • unloadCard(card, target) — called when the card is closed or the extension tears down. Clean up your content.

Because loadCard runs inside your extension, it has direct closure access to the api object from your lifecycle hooks — no cross-frame messaging needed.

React example:

Copy
import { createRoot, type Root } from 'react-dom/client';
import type { IExtensionParameters, IExtensionConfig, IReviewInterfaceApi } from 'reviewapi';
import { MyCard } from './MyCard';

export default function(parameters: IExtensionParameters): IExtensionConfig {
  let api: IReviewInterfaceApi;
  let root: Root | undefined;

  return {
    id: 'acme.extension',
    name: 'ACME Extension',
    lifecycle: {
      apiready: (readyApi) => {
        api = readyApi;
      },
    },
    cards: [
      {
        id: 'acme-sidebar-card',
        title: 'ACME Panel',
        singleton: true,
        location: {
          layoutId: 'review',
          paneId: 'ri-review-right-accordion',
          dockIndex: 0,
        },
        loader: {
          custom: {
            loadCard: async (card, target) => {
              root = createRoot(target);
              root.render(<MyCard api={api} card={card} />);
            },
            unloadCard: async (card, target) => {
              root?.unmount();
              root = undefined;
            },
          },
        },
      },
    ],
  };
}

IFrame loader

IIframeCardLoader loads card content from a URL or a Relativity resource file into an <iframe>:

Copy
loader: {
  iframe: {
    url: 'https://example.com/my-card/index.html',
    // or, for a Relativity resource file:
    // fileName: 'review.my-card.html',
  },
},

From within the iframe, access the card object via window.frameElement.reviewCard:

Copy
const card = window.frameElement.reviewCard;

Cross-origin note: If your extension is CDN-hosted, the CDN origin may differ from the Review Interface origin. This can prevent the iframe from accessing window.frameElement, window.parent, or window.top due to browser cross-origin restrictions. The custom loader avoids this problem entirely by keeping card content in the same execution context as the extension.

autoResolveLoad can be set to false to take manual control over when the card reports itself as loaded — useful if your iframe content needs to perform async initialization before it's ready to display. Call card.completeFrameLoad() or card.failFrameLoad(error) when ready.

ViewModel loader

IViewModelCardLoader loads card content using Aurelia's view/viewmodel system. This loader is specific to the Aurelia framework and is not recommended for new extensions.


Toolbar controls

Extensions can add buttons and other controls to the viewer toolbar via viewerToolbarControls on the extension config. This function is called once per viewer type as each viewer is activated, receiving the API, the viewer type, and the toolbar instance.

Copy
import type { IExtensionParameters, IExtensionConfig } from 'reviewapi';

export default function(parameters: IExtensionParameters): IExtensionConfig {
  return {
    id: 'acme.extension',
    name: 'ACME Extension',
    viewerToolbarControls: (api, viewerType, viewerToolbar) => {
      // Add a button to all viewer types
      const buttonControl = api.toolbar.createToolbarControl({
        type: 'button',
        id: 'acme-toolbar-button',
        title: 'ACME Action',
        imageUrl: parameters.getResourceFileUrl('review.acme-icon.png'),
        onClick: () => {
          // handle click
        },
      });
      viewerToolbar.addControl(buttonControl, 'right');

      // Add a viewer-specific control
      if (viewerType === 'native') {
        const nativeControl = api.toolbar.createToolbarControl({
          type: 'button',
          id: 'acme-native-button',
          title: 'Native Action',
          onClick: () => { /* ... */ },
        });
        viewerToolbar.addControl(nativeControl, 'right');
      }
    },
  };
}

For the full toolbar API, see Toolbars.


Context menu items

Extensions can add items to the right-click context menu in the viewer via viewerContextMenus on the extension config. This function is called per viewer type and returns an array of menu item configs.

Copy
import type { IExtensionParameters, IExtensionConfig } from 'reviewapi';

export default function(parameters: IExtensionParameters): IExtensionConfig {
  return {
    id: 'acme.extension',
    name: 'ACME Extension',
    viewerContextMenus: (api, viewerType) => {
      const items = [];

      // Add to all viewer types
      items.push({
        text: 'ACME Action',
        order: 100,
        onClickCallback: (reviewData, api) => {
          const docId = reviewData.queuePointer.item.artifactId;
          // handle click
        },
      });

      // Add only for native and text viewers
      if (viewerType === 'native' || viewerType === 'text') {
        items.push({
          text: 'Search selected text',
          order: 110,
          onClickCallback: (reviewData, api) => {
            const selected = reviewData.getSelectedText();
            if (selected) {
              api.highlights.setRecentSearchTerm(selected, 'literal');
            }
          },
        });
      }

      return items;
    },
  };
}

Menu items support child items for nested menus, permission checks to conditionally hide items, and an onBuildCallback to modify items dynamically at the time the menu is built (for example, to disable an item based on current document state).

For the full context menu API, see Context Menus.


Document content overlays

To render highlights directly on document content — rather than in a panel — use the highlighting APIs. These are separate from the extension config properties above and are set up via lifecycle hooks.

  • Content Highlighting Framework — register one or more highlighting features via the contentHighlighting property on the extension config. The framework manages render function invocation, cross-viewer sync, caching, and optional card integration. Best for data-driven highlights fetched from an API.

  • Transient Highlights — create and manage highlights directly on the active viewer via api.viewer.mainCollection.activeViewer.transientHighlight. Best for interactive or event-driven highlighting where you need direct control.

See Highlights for guidance on which approach fits your use case.

Return to top of the page
Feedback