Documentation
Viewer

Viewer

A UI component that renders a multicanvas IIIF item viewer with pan-zoom support for Image via OpenSeadragon (opens in a new tab) and Video and Sound content resources using the HTML video element (opens in a new tab).

Manifest or Collection

Features

Provide a IIIF Presentation API (opens in a new tab) Manifest or Collection and the component:

  • Renders a multi-canvas Video, Sound, and Image viewer
  • Renders thumbnails as navigation between canvases
  • Renders annotations with the motivation (opens in a new tab) of supplementing with a content resource having the format of text/vtt for Video and Sound
  • Video and Sound are rendered within a HTML5 <video> element
  • Image canvases are renderered with OpenSeadragon (opens in a new tab)
  • Supports HLS streaming for Video and Audio canvases
  • Supports IIIF Collections and toggling between child Manifests
  • Supports placeholderCanvas for Image canvases.

Installation

  npm install @samvera/clover-iiif

Usage

React

Add the Viewer component to your jsx or tsx code.

import Viewer from "@samvera/clover-iiif/viewer";

Render Viewer with a IIIF Manifest or Collection URI. The only required prop is the iiifContent, which is the URI of the IIIF Manifest or Collection.

<Viewer iiifContent="https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif" />

Vanilla JavaScript

The Viewer can also be implemented in Vanilla Javascript by use of a web component. This web component example sources a registered <clover-viewer> web component.

<html>
  <head>
    <title>Clover IIIF - Viewer - Web Component</title>
    <meta charset="UTF-8" />
  </head>
  <body>
    <script src="https://www.unpkg.com/@samvera/clover-iiif@latest/dist/web-components/index.umd.js"></script>
 
    <clover-viewer
      id="https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif"
    />
  </body>
</html>

Next.js

Implementation with Next.js requires a dynamic import (opens in a new tab) utilizing next/dynamic. This is due to Next's node compilation method creating issue with an OpenSeadragon (a dependency of Clover IIIF) assumption of a browser document object.

import dynamic from "next/dynamic";
 
const Viewer = dynamic(
  () => import("@samvera/clover-iiif").then((Clover) => Clover.Viewer),
  {
    ssr: false,
  },
);
 
const MyCustomViewer = () => {
  const iiifContent =
    "https://api.dc.library.northwestern.edu/api/v2/collections/c373ecd2-2c45-45f2-9f9e-52dc244870bd?as=iiif";
 
  return <Viewer iiifContent={iiifContent} />;
};

API Reference

Viewer can configured through an options prop, which will serve as a object for common options.

PropTypeRequiredDefault
iiifContentstringYes
iiifContentSearchQuerySee Content SearchNo
canvasIdCallbackfunctionNo
customDisplaysSee Custom DisplaysNo
customThemeobjectNo
pluginsSee PluginsNo
optionsobjectNo
options.backgroundstring CSS (opens in a new tab)Notransparent
options.canvasBackgroundColorstring CSS (opens in a new tab)No#1a1d1e
options.canvasHeightstring CSS (opens in a new tab)No500px
options.ignoreCaptionLabelsstring[]No[]
options.openSeadragonOpenSeadragon.OptionsNo
options.informationPanelSee Information PanelNo
options.requestHeadersIncomingHttpHeadersNo{ "Content-Type": "application/json" }
options.showDownloadbooleanNotrue
options.showIIIFBadgebooleanNotrue
options.showTitlebooleanNotrue
options.withCredentialsbooleanNofalse
options.contentSearchSee Content SearchNo
  • Options canvasBackgroundColor and canvasHeight will apply to both <video> elements and the OpenseaDragon canvas.
  • Option withCredentials being set as true will inform IIIF resource requests to be made using credentials (opens in a new tab) such as cookies, authorization headers or TLS client certificates.
  • Option options.openSeadragon will grant you ability to override the OpenSeadragon default options (opens in a new tab) set within the Clover IIIF Viewer to adjust touch and mouse gesture settings and various other configurations.

Canvas Height

The height of the canvas can be set using the options.canvasHeight prop. This prop accepts a string value that is a valid CSS height value. The default value is 500px.

Automatic Height

If the height is set to auto or 100%, the Viewer will expand to the height of its wrapping container element. The wrapping element must have a position relative, along with a defined height for this to display as expected. Be aware to set a z-index value of 0 or an applicable value within your consuming application to ensure the viewer does not conflict with other page elements.

export default function App() {
  const iiifContent =
    "https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif";
 
  const options = {
    canvasHeight: "auto", // or "100%"
  };
 
  return (
    <div style={{ position: "relative", height: "80vh", zIndex: "0" }}>
      <Viewer iiifContent={iiifContent} options={options} />
    </div>
  );
}

Information Panel

The information panel is a collapsible panel that displays information about the current Manifest and renders supplementing resources for the active canvas. It is rendered by default, but can be configured to be hidden or to render only certain tabs.

PropTypeRequiredDefault
options.informationPanel.openbooleanNotrue
options.informationPanel.vtt.autoScrollSee Auto ScrollNotrue
options.informationPanel.renderAboutbooleanNotrue
options.informationPanel.renderAnnotationbooleanNotrue
options.informationPanel.renderSupplementingbooleanNotrue
options.informationPanel.renderTogglebooleanNotrue
options.informationPanel.renderContentSearchbooleanNotrue
options.informationPanel.defaultTabstringNo

If renderAbout is true, Clover will use the About tab as the default tab. Use options.informationPanel.defaultTab to set the default tab.

If you want content search to be the default tab, use manifest-content-search

<Viewer
  iiifContent="https://example.com/manifest/1"
  options={{
    informationPanel: {
      defaultTab: "manifest-content-search",
    },
  }}
/>

If the manifest has annotations, and you want one of the anotations to be the default tab, use the id from the annotation page.

{
  "@context": ["http://iiif.io/api/presentation/3/context.json"],
  "type": "Manifest",
   ...
  "items": [
    {
      "id": "http://example.com/manifest/canvas/1",
      "type": "Canvas",
      "height": 1000,
      "width": 2000,
      "annotations": [
        {
          "id": "http://example.com/manifest/annotation/1",
          "type": "AnnotationPage",
          "items": [...]
        }
      ]
    }
  ]
}
<Viewer
  iiifContent="https://example.com/manifest/1"
  options={{
    informationPanel: {
      defaultTab: "http://example.com/manifest/annotation/1",
    },
  }}
/>

Auto Scroll

When VTT annotations are displayed, the Clover IIIF Viewer can automatically scroll the information panel to keep the currently active caption in view. Whether it does so (and how) is governed by the informationPanel.vtt.autoScroll configuration option. The possible values are:

  • { behavior: (behavior), block: (block) }: auto-scroll using the given settings (see below).
  • true: Auto-scroll using the default behavior. This is equivalent to { behavior: "smooth", block: "center" }.
  • false: Do not auto-scroll.

The settings take the form { behavior: "auto" | "instant" | "smooth", block: "center" | "end" | "nearest" | "start" }, and have the same effect as the scrollIntoViewOptions object documented with the Element.scrollIntoView() (opens in a new tab) Web API method. (The inline option does not apply since there is no horizontal scrolling involved.)

Custom Displays

Clients may wish to use their own display components (for example a PDF Viewer, or an audio player, etc). To configure custom displays, use the customDisplays prop, which is an array of objects defining display and target properties. See an example implementation

display.component is a custom React component, and display.componentProps are pass-through props which Viewer will attach to your Custom Display component. The target object provides two methods of matching a Canvas to a Custom Display: target.canvasId which is a manifest's canvas id. Or by target.paintingFormat (ie. application/pdf) which is the body.type in a canvas's Annotation of type "painting".

PropTypeRequiredDefault
customDisplays.display.componentReact.NodeNo
customDisplays.display.componentPropsobjectNo
customDisplays.target.canvasIdstring[]No
customDisplays.target.paintingFormatstring[]No

Content search

Clover supports IIIF content search v2. In order to display content search in the information panel, add a content search service to the manifest.

{
  "@context": ["http://iiif.io/api/presentation/3/context.json"],
  "type": "Manifest",
  ...
  "service": [
    {
      "id": "https://example.com/search",
      "type": "SearchService2"
    }
  ]
}
PropTypeRequiredDefault
iiifContentSearchQuery.qstringNo
options.contentSearch.searchResultsLimitnumberNo20
options.contentSearch.overlays.backgroundColorstringNo#ff66660
options.contentSearch.overlays.borderColorstringNo#990000
options.contentSearch.overlays.borderTypestringNosolid
options.contentSearch.overlays.borderWidthstringNo1px
options.contentSearch.overlays.opacitystringNo0.5
options.contentSearch.overlays.renderOverlaysbooleanNotrue
options.contentSearch.overlays.zoomLevelnumberNo4

Many sites have a search functionality that returns a list objects that match the search terms. Clicking on a search result will display a details page for one object. If you want to highlight the search terms when the Clover image viewer loads on the details page, use iiifContentSearchQuery when setting up the viewer.

<Viewer
  iiifContent="https://example.com/manifest/1"
  iiifContentSearchQuery={{
    q: "my search terms",
  }}
/>

options.contentSearch.searchResultsLimit is the maximum number of search results to display per canvas in the information panel. If set to undefined, Clover will show all search results.

By default, Clover's content search will draw light red boxes in the image viewer for each search result returned by the search service. You can set the appearance of the highlighted boxes using options.contentSearch.overlays. If you don't want the search results to be highlighted, set options.contentSearch.overlays.renderOverlays to false.

When you click on the list of search results, Clover will pan and zoom to the location of that search result. You can set the zoom level using options.contentSearch.overlays.zoomLevel. A small zoom level will zoom in real close; a large zoom level will zoom in less.

Deprecated Options

PropIn Favor OfDeprecated
idiiifContentv2.0.0
manifestIdiiifContentv2.0.0
options.renderAboutoptions.informationPanel.renderAboutv2.0.3
options.showInformationToggleoptions.informationPanel.renderTogglev2.0.3

Basic Configuration

Example customization of various options.

const options = {
  // Primary title (Manifest label) for top level canvas.  Defaults to true
  showTitle: false,
 
  // IIIF Badge and popover containing options.  Defaults to true
  showIIIFBadge: false,
 
  // Ignore supplementing canvases by label value that are not for captioning
  ignoreCaptionLabels: ['Chapters'],
 
  // Override canvas background color, defaults to #1a1d1e
  canvasBackgroundColor: "#000",
 
  // Set canvas zooming onScoll (this defaults to false)
  openSeadragon: {
    gestureSettingsMouse: {
      scrollToZoom: true;
    }
  }
}
 
<Viewer
  iiifContent="https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif"
  options={options}
/>

Active Canvas

Example on using canvasIdCallback to return to your consuming application the active canvas ID. This will return as a string.

const iiifContent =
  "https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif";
 
const handlCanvasIdCallback = (activeCanvasId) => {
  if (activeCanvasId) console.log(activeCanvasId);
};
 
return (
  <Viewer iiifContent={iiifContent} canvasIdCallback={handlCanvasIdCallback} />
);

Captions

WebVTT content resources are the source for both content mapped closed captioning <track/> elements in the HTML 5 video player and to the navigator panel adjacent to it. You may ignore these resources as tracks if they are not intended for closed captioning or subtitling by string values matching the label of the content resource. This is a manual option within the viewer as there is no defined way for a manifest to prescribe motivation for these resources beyond supplementing.

export default function App() {
  const iiifContent =
    "https://raw.githubusercontent.com/samvera-labs/clover-iiif/main/public/fixtures/iiif/manifests/captions.json";
 
  const options = {
    ignoreCaptionLabels: ["Chapters"],
  };
 
  return <Viewer iiifContent={iiifContent} options={options} />;
}

Custom Theme

You may choose to override the base theme by setting optional colors and fonts. Naming conventions for colors are limited to those shown in the config example below.

const iiifContent =
  "https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif";
 
const customTheme = {
  colors: {
    /**
     * Black and dark grays in a light theme.
     * All must contrast to 4.5 or greater with `secondary`.
     */
    primary: "#37474F",
    primaryMuted: "#546E7A",
    primaryAlt: "#263238",
 
    /**
     * Key brand color(s).
     * `accent` must contrast to 4.5 or greater with `secondary`.
     */
    accent: "#C62828",
    accentMuted: "#E57373",
    accentAlt: "#B71C1C",
 
    /**
     * White and light grays in a light theme.
     * All must must contrast to 4.5 or greater with `primary` and  `accent`.
     */
    secondary: "#FFFFFF",
    secondaryMuted: "#ECEFF1",
    secondaryAlt: "#CFD8DC",
  },
  fonts: {
    sans: "'Helvetica Neue', sans-serif",
    display: "Optima, Georgia, Arial, sans-serif",
  },
};
 
return <Viewer iiifContent={iiifContent} customTheme={customTheme} />;

CSS Classes

Additional CSS classes are made available on structural HTML elements in the Viewer, which may be referenced in a client's own CSS files/style definitions to further customize the Viewer's appearance. You may inspect the DOM to see classes applied to each element, but in general it follows a pattern similar to:

<div class="clover-viewer">
  <header class="clover-viewer-header" />
  <div class="clover-viewer-content">
    <div class="clover-viewer-painting">...</div>
  </div>
</div>

Request Headers

In some cases, a client may need to request Manifest or Collection resources with custom request headers, ex: Authorization. This can be done by passing a requestHeaders object to the options prop. This object will be passed to the request call made by the Viewer. Accepted header keys are defined in the IncomingHttpHeaders (opens in a new tab) interface.

const iiifContent =
  "https://api.dc.library.northwestern.edu/api/v2/works/8a833741-74a8-40dc-bd1d-c416a3b1bb38?as=iiif";
 
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
 
return (
  <Viewer
    iiifContent={iiifContent}
    options={{
      requestHeaders: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
    }}
  />
);

Custom canvas displays

Clients may wish to use their own display components instead of Clover Viewer's default displays (OpenSeadragon (opens in a new tab) for images and HTML Video Player (opens in a new tab) for audio/video). The Viewer component allows a client to target individual canvas items in a IIIF Manifest by either direct reference to a canvas id or format (ie. video/ogg). See the Type Definition below for CustomDisplay, and an example implementation.

import AnotherCustomDisplay from "./AnotherCustomDisplay";
 
type CustomDisplay = {
  display: {
    component: React.ElementType;
    componentProps: {
      // Any custom props you want to pass to your component
      [key: string]: any;
    };
  };
  target: {
    canvasId: string[];
    paintingFormat: string[]; // "application/pdf" or "application/epub+zip"
  };
};
 
function MyCustomDisplay({ id, annotationBody, ...restProps }: CustomDisplay) {
  return (
    <div>
      <h1>My Custom Display</h1>
      <p>Canvas ID: {id}</p>
      <p>Annotation Body:</p>
      <pre>{JSON.stringify(annotationBody)}</pre>
      <p>Custom props:</p>
      <pre>{JSON.stringify(restProps)}</pre>
      ...your display here
    </div>
  );
}
 
<Viewer
  iiifContent={iiifContent}
  customDisplays={[
    {
      display: {
        component: MyCustomDisplay,
        componentProps: {
          foo: "bar",
        },
      },
      target: {
        canvasId: [
          "https://uri-for-a-canvas-id/access/0",
          "https://uri-for-a-canvas-id/access/1",
        ],
      },
    },
    {
      display: {
        component: AnotherCustomDisplay,
      },
      target: {
        paintingFormat: ["application/pdf", "image/gif"],
      },
    },
  ]}
/>;

The Viewer component will pass the following props to your custom display component:

  • id: The canvas id for the resource being rendered. This may be helpful if you wanted to use the canvas id to fetch additional data from your application's API.
  • annotationBody: The body value for a canvas Annotation item with motivation "painting".
{
  "id": "https://uri-for-a-canvas-id/access/0",
  "type": "Annotation",
  "motivation": "painting",
  "body": {
    "format": "application/pdf",
    "height": 1686,
    "id": "http://localhost:3000/media/pdf/file-sample_150kB.pdf",
    "width": 1192
  }
}

See a complete recipe for a PDF Viewer (opens in a new tab) using custom canvas displays.


Plugins

Clover supports 3rd-party plugins as a way to add more functionality to Clover. To add plugins, use the plugins prop. plugins will accept an array of objects, with each object representing a plugin.

People can use prebuilt plugins or create their own plugins. To install a prebuilt plugin, people should follow the instructions for the particular plugin.

Here's an example of using two plugins. PluginA adds a button to imageViewer.controls. PluginB adds a tab to informationPanel.

import PluginA from "plugin-A";
import PluginB from "plugin-B";
 
export default function App() {
  return (
    <Viewer
      iiifContent={iiifContent}
      plugins={[
        {
          id: "Plugin A",
          imageViewer: {
            controls: {
              component: PluginA,
            },
          },
        },
        {
          id: "Plugin B",
          informationPanel: {
            component: PluginB,
            componentProps: {
              objectId: 456,
            },
            label: { none: ["My Plugin"] },
          },
        },
      ]}
    />
  );
}

plugin

PropTypeRequiredDefault
idstringyes
imageViewer.controlsobjectNo
imageViewer.controls.componentReact Componentyes
imageViewer.controls.componentPropsobjectno
informationPanelobjectNo
informationPanel.componentReact Componentyes
informationPanel.componentPropsobjectno
informationPanel.labelobjectyes

id - unique id for the plugin

imageViewer.controls - If you want to add a custom component to image viewer controls, use imageViewer.controls.

imageViewer.controls.component - The component to render.

imageViewer.controls.componentProps - Props passed to the controls component.

informationPanel - If you want to add a custom component to the information panel, use informationPanel.

informationPanel.component - The component to render.

informationPanel.componentProps - Props passed to the information panel component.

informationPanel.label - The text that is displayed in the tab.

Instructions for creating a plugin

Clover offers these features to support plugins:

  • Renders custom React components in certain areas of the Clover Viewer. Areas include image viewer controls and information panel.
  • Gives plugins the ability to access and change the state of the Clover Viewer via props
  • Pass in props to the plugin components
PropDescription
canvasactive canvas object
useViewerDispatchReact hook to update the Viewer context store (opens in a new tab)
useViewerStateReact hook to access the Viewer context store (opens in a new tab)

Here is an example plugin that has both imageViewer.controls and informationPanel. Plugin components have access to canvas, useViewerDispatch, and useViewerState via props.

plugin

// PluginButton.tsx
 
export default function PluginButton(props) {
  const { canvas, useViewerDispatch, useViewerState } = props;
 
  // use useViewerState to access to viewer state properties such as openSeadragonViewer, etc
  const viewerState = useViewerState();
  const { openSeadragonViewer, activeManifest } = viewerState;
 
  // use useViewerDispatch to update viewer state
  const dispatch: any = useViewerDispatch();
 
  function clickHandler() {
    dispatch({
      type: "updateOSDImageLoaded",
      OSDImageLoaded: false,
    });
  }
 
  return (
    <button
      onClick={clickHandler}
      style={{
        backgroundColor: "var(--colors-primary)",
        borderRadius: "2rem",
        width: "2rem",
        margin: "0px 0px 0px 0.618rem",
      }}
    >
      <b>P</b>
    </button>
  );
}
 
// PluginInfoPanel.tsx
 
export default function PluginInfoPanel(props) {
  const {
    canvas,
    useViewerDispatch,
    useViewerState,
 
    // componentProps
    objectId,
  } = props;
 
  return (
    <div style={{ padding: "0px 1.618rem 2rem" }}>
      <p>Plugin Info Panel</p>
      <p>objectId: {objectId}</p>
    </div>
  );
}
 
// App.tsx
 
import PluginButton from "PluginButton";
import PluginInfoPanel from "PluginInfoPanel";
 
export default function App() {
  return (
    <Viewer
      iiifContent="https://iiif.io/api/cookbook/recipe/0001-mvm-image/manifest.json"
      plugins={[
        {
          id: "Demo",
          imageViewer: {
            controls: {
              component: PluginButton,
            },
          },
          informationPanel: {
            component: PluginInfoPanel,
            label: { none: ["My Plugin"] },
            componentProps: {
              objectId: 456,
            },
          },
        },
      ]}
    />
  );
}

If the imageViewer.controls.component and informationPanel.component need to share state, wrap the Viewer in a context provider that is provided by the plugin.

import PluginButton from "PluginButton";
import PluginInfoPanel from "PluginInfoPanel";
import { PluginProvider } from "PluginContext";
 
export default function App() {
  return (
    <PluginProvider>
      <Viewer
        iiifContent="https://iiif.io/api/cookbook/recipe/0001-mvm-image/manifest.json"
        plugins={[
          {
            id: "Demo",
            imageViewer: {
              controls: {
                component: PluginButton,
              },
            },
            informationPanel: {
              component: PluginInfoPanel,
              label: { none: ["My Plugin"] },
              componentProps: {
                objectId: 456,
              },
            },
          },
        ]}
      />
    </PluginProvider>
  );
}