Skip to Content

Technical overview

How the package works, in React and as web components: the same parts, read the same way, in either. The code switch shows yours.

Entry points

ImportWhat it is
@nulib/cloverThe Viewer (the default export), and every part, hook and helper (named).
@nulib/clover/templatesThe templates: Default, Document and Gallery.
@nulib/clover/mapMap<clover-map>, for navPlace and georeferenced images. The Viewer adds its Map tab itself.
@nulib/clover/threeregisterThreeEngine, to draw a Scene with three.js, which you install.
@nulib/clover/web-components<clover-viewer>, and every part as an element, for a page without React.
@nulib/clover/web-components/viewer<clover-viewer> alone, built whole, for a page that shows a Viewer and nothing else of Clover’s.
@nulib/clover/web-components/threethree.js for the elements’ Scene, bundled.
@nulib/clover/styles.cssEvery part’s styles, and the Default template’s.
@nulib/clover/templates.cssThe other templates’ styles.

The package is ES modules only.

React 18 and 19 are supported. Import the parts from @nulib/clover, by name or as Clover, and your bundler keeps only those you use.

The elements are built on their own, with Preact  standing in for React, and carry Clover’s stylesheet. index.umd.js is a classic script that loads the elements’ module from beside itself, so it works from any CDN path. On a page with modules, load web-components/index.mjs with <script type="module"> instead.

<script src="https://cdn.jsdelivr.net/npm/@nulib/clover/dist/web-components/index.umd.js"></script>

A page that shows a Viewer and nothing else of Clover’s can load the Viewer’s own entry, built whole: web-components/viewer/index.umd.js (or @nulib/clover/web-components/viewer in a bundler) loads 102 kB to first view, where the one for every element loads 122 kB for the same page. Load one entry on a page, not both: each brings its own copy of Clover.

If you serve Clover’s files from your own server, copy the whole dist/web-components folder, not viewer/ alone: the Viewer’s entry loads what the other entry has too (the map, streaming video, the languages) from the folder above its own.

The Viewer

Viewer draws its template, Default unless you pass another from @nulib/clover/templates, and hands every other prop to it. iiifContent takes a URL, a content state, or a IIIF resource as an object. Every prop is in the Viewer’s API reference.

<clover-viewer> shows the Viewer in any of its templates: Default unless template names another, which loads the first time one is shown, its styles with it. Every attribute is in the Viewer’s API reference.

<clover-viewer template="document" iiif-content="https://iiif.io/api/cookbook/recipe/0024-book-4-toc/manifest.json" ></clover-viewer>

A template takes its own attributes and ignores another’s: size does nothing in Default. false is read in any case. An items value not in the list shows the slider, with a warning in the console. With header="false" there is no title, and the panel’s sections take heading-level themselves.

The iiifContent property takes a IIIF object as well, which an attribute cannot carry, set before the element’s script has loaded or after:

document.querySelector("clover-viewer").iiifContent = manifestJson;

Events

What React hands a callback, an element sends as an event: clover- and what happened, bubbling, so a page can listen for it anywhere above the element. Its detail holds what the callback is given.

React propEventdetail
onCanvasChangeclover-canvas-changeclover-canvas-change{ canvasId }
onManifestChangeclover-manifest-changeclover-manifest-change{ manifestId }, in a Collection
onStatusChangeclover-status-changeclover-status-change{ status, error }, as it loads
onContentStateChangeclover-content-state-changeclover-content-state-change{ json, encoded }, where the reader settles
viewer.addEventListener("clover-canvas-change", (event) => { console.log(event.detail.canvasId); });

<clover-root> sends the same events as <clover-viewer>; <clover-items> and <clover-map> send clover-select when an item or a place is chosen (cancel it, with preventDefault, to keep a link from being followed), <clover-collection-select> sends clover-change, and <clover-root>, <clover-body> and <clover-canvas> send clover-ready (parts of your own).

Languages

Clover’s own words follow the page’s <html lang>, then the reader’s browser, in either. initCloverI18n sets their language, or adds strings, for every Clover part on the page; see i18n.

import { initCloverI18n } from "@nulib/clover"; initCloverI18n({ lng: document.documentElement.lang });

The elements carry their own copy of Clover’s translator, so a page calls it through them: from the module, or from any element’s class in a classic script.

// With modules. import { initCloverI18n } from ".../web-components/index.mjs"; initCloverI18n({ lng: document.documentElement.lang }); // From a classic script, once the element is defined. customElements.get("clover-viewer").initCloverI18n({ lng: "fr" });

Where that language is not the page’s, Clover marks each of its own words with lang; see Languages.

Parts

Every part is a component, read inside a Root, among your own markup. Each has its page under Components, and Composing lists them all.

The parts compose in HTML inside a <clover-root>, as they do in JSX inside a Root. Each draws its part into itself, wherever the page puts it in the root, and the page’s own markup between them stays as written.

<clover-root iiif-content="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json" > <clover-label as="h2"></clover-label> <clover-body style="height: 60vh"></clover-body> <p>Now showing <clover-label of="canvas"></clover-label>.</p> <clover-previous></clover-previous> <clover-position></clover-position> <clover-next></clover-next> <clover-summary></clover-summary> <clover-metadata></clover-metadata> </clover-root>
ElementsWhat they drawAttributes
<clover-root>Nothing of its own: it loads what it is given, and its parts read where the reader is.iiif-content (or the iiifContent property), canvas-id and manifest-id (where it opens; changed later, they move the reader), language, search-query, annotations.
<clover-body>The view, in a box as tall as --clover-body-height (60vh) or its own style; a <button> in it is one of its controls.controls and navigator (false turns one off), placeholder, animate-annotations; parts of your own.
<clover-canvas>Clover’s renderer for images, video and sound: alone, what it is given; in a root, its view.iiif-content, src and tiled, controls, navigator, and more.
<clover-items>Thumbnails of the views, or of the Collection’s members.layout (slider, list, grid or pager), of, navigation, thumbnail-size; alone, iiif-content, header, heading-level.
<clover-previous>, <clover-position>, <clover-next>Stepping through the views, and where the reader is.of (views or members).
<clover-status>What the root is doing: loading, or why it failed.loading="false", where the Body shows its own.
<clover-label>, <clover-thumbnail>, <clover-summary>, <clover-metadata>, <clover-required-statement>, <clover-rights>, <clover-homepage>, <clover-see-also>, <clover-rendering>, <clover-part-of>, <clover-id>One IIIF property each, of the Manifest or the Canvas in view.of (manifest or canvas); value, its own as text, a URL or JSON, which alone, outside a root, it shows; as on a label (a heading, h1 to h6) and a summary; size and alt on a thumbnail.
<clover-value>Any language map, as text, HTML or Markdown.value, format, heading-level, as.
<clover-about>, <clover-annotations>, <clover-collection>, <clover-collection-select>, <clover-contents>, <clover-search>, <clover-transcript>, <clover-download>The information panel’s sections, each on its own.heading-level (one under the heading they sit beneath), and each its own.
<clover-map>The root’s places and georeferenced images; alone, those of its iiif-content.overlay, overlay-opacity, style-url, scroll-zoom.
<clover-panel>, <clover-tab>The information panel, in tabs or stacked boxes: the sections Clover has data for, or <clover-tab>s of your own.layout, value, heading-level, closable; a tab’s value and label (or a slot="label" child).
<clover-panel-toggle>A button that shows and hides the panel, or anything, by its id; in a <clover-body>, one of the Body’s controls.controls.
<clover-split>, <clover-split-pane>, <clover-split-handle>Panes side by side, and the bars between them, which the reader drags, clicks or moves by keyboard.The split’s storage-key; a pane’s size, min, max and name.
<clover-tip>Clover’s popover, in a button of yours: its label and its key, as Clover’s controls show theirs.label, keys, glyph, lang.

Every attribute, property and event is in the manifest. A part of a root draws nothing outside one, and says so in the console. Give an element no aria-label or role: it is a box no reader is told of, and Clover names what it draws itself; a landmark of the page’s own around an element names it otherwise. The one exception is a Split’s handle, which is the separator itself: name it with aria-label where “Resize panels” is not the right word.

Layouts

Split lays panes side by side, Panel holds the information panel’s sections or Tabs of your own, and PanelToggle shows and hides it: see Panes side by side and Panel.

A layout as the Default template’s, written in HTML: the Body with the panel’s toggle among its controls, a bar the reader can move, and the information panel beside it, with tabs of your own.

<clover-root iiif-content="https://iiif.io/api/cookbook/recipe/0024-book-4-toc/manifest.json" > <clover-split storage-key="my-layout" style="height: 80vh"> <clover-split-pane min="16rem"> <clover-label as="h2"></clover-label> <clover-body style="height: 60vh"> <clover-panel-toggle controls="panel-pane"></clover-panel-toggle> </clover-body> <clover-items></clover-items> </clover-split-pane> <clover-split-handle></clover-split-handle> <clover-split-pane id="panel-pane" size="24rem" min="14rem"> <clover-panel> <clover-tab value="about" label="About"> <clover-about heading-level="3"></clover-about> </clover-tab> <clover-tab value="notes"> <span slot="label">Notes <em>(2)</em></span> <p>Our own notes on this book.</p> </clover-tab> </clover-panel> </clover-split-pane> </clover-split> </clover-root>
  • The panel has the sections Clover has data for, or one tab for each <clover-tab> in it, in their order. A tab’s content stays where you wrote it until its section first opens, and then moves there. Its label is its label, or a copy of its slot="label" child, for markup. value is the tab it opens on, and changing it shows another; the reader’s choice sends clover-value-change. With closable, it has a close button, which sends clover-close (cancel it to keep the panel shown) and hides what its toggle shows.
  • The toggle shows its target while the target is not hidden, so write hidden on the panel to start it shut, and hide or show it yourself whenever you like: the toggle follows. Each press sends clover-open-change; cancel it to leave the panel as it is.
  • The Split’s elements are the panes and the bars themselves, as Split lays them out in React: one pane beside each handle has a size, and the other takes what is left. A handle beside a pane you hide is hidden with it.
  • Slots. <clover-status> shows your own words while the root loads (slot="loading") and if it fails (slot="error"), as copies, so give them no ids.
  • Functions. renderValue on <clover-metadata>, renderEntry on <clover-annotations> and renderItem on <clover-items> are properties that take a function returning a node or text: Clover shows it where it would draw its own.

Alone

Three parts need no Root: Canvas draws what it is given, Items lists a Collection’s members or a Manifest’s views, and Map maps the places of what it is given. Each of their pages shows it alone.

Three elements need no root. <clover-canvas> draws what it is given: a IIIF resource by URL (a Manifest gives its first Canvas), an image by src (with tiled, a IIIF Image service), or Canvases as its canvases property. <clover-items> lists a Collection’s members, or a Manifest’s views, as links. <clover-map> maps the places of what it is given.

<clover-canvas iiif-content="https://iiif.io/api/cookbook/recipe/0005-image-service/manifest.json" style="height: 60vh" ></clover-canvas> <clover-items iiif-content="https://iiif.io/api/cookbook/recipe/0032-collection/collection.json" heading-level="3" ></clover-items> <clover-map iiif-content="https://iiif.io/api/cookbook/recipe/0240-navPlace-on-canvases/manifest.json" ></clover-map>

<clover-canvas> takes controls and navigator as <clover-body> does, label (the name of an image given by src), placeholder (false opens a Canvas past its placeholder), viewing-direction (how a spread is laid out) and animate-annotations. It sends clover-annotation-select and clover-annotation-active ({ id }), clover-caption-change ({ caption }) and clover-ended. Inside a <clover-root>, given nothing, it shows the root’s view; place <clover-body> there instead, which adds a height and the loading mark.

Parts of your own

Hooks read from the nearest Root, and each re-renders only when what it reads changes: useNavigation(), useVisibleCanvases(), useActiveCanvas() and the rest, in Parts of your own. Canvas and Body hand their handle to onReady; renderers on a Root, Body or Viewer draws views of your own (Renderers of your own); and controlButtons puts buttons of yours in place of the Body’s controls (Controls of your own).

A script of the page’s reads a root as a React part reads one with Clover’s hooks. Once the root has loaded, root.clover is its Clover, and clover-ready says so. Each hook is a method of the same name (useNavigation() is navigation(), useVisibleCanvases() is visibleCanvases()), with the table of contents (contents(), as tableOfContents gives React’s), state and subscribe, the actions (next(), goTo(id), search(query)…), the channels (viewport, media…), and localize, t and langOf for the root’s language.

<clover-root id="book" iiif-content="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json" > <clover-body></clover-body> <p id="where"></p> <button type="button" id="next">Next view</button> </clover-root> <script type="module"> const root = document.getElementById("book"); const where = document.getElementById("where"); const show = () => { const { position, total } = root.clover.navigation(); where.textContent = `View ${position + 1} of ${total}`; }; root.addEventListener("clover-ready", () => { show(); root.clover.subscribe(show); }); document .getElementById("next") .addEventListener("click", () => root.clover?.next()); </script>
  • The Canvas’s handle. <clover-canvas> and <clover-body> keep it as handle once their view is laid out, as onReady hands it to a React host, and send clover-ready: body.handle.zoomBy(2), body.handle.home(). A clover-ready is the element’s own, and does not bubble, as load does not.

  • Renderers. renderers on a <clover-root>, <clover-body> or <clover-viewer> draws views of your own, as React’s do. Each claims views by canvasId, format or match, and its render(container, view) draws one into a box filling the Body and returns what undoes it. view holds the view’s Canvases, their painting bodies and the root’s clover, and the Body’s controls stay over it.

    root.renderers = [ { format: ["model/gltf-binary"], render(container, { bodies }) { const model = showModel(container, bodies[0].id); return () => model.dispose(); }, }, ];
  • Controls of your own. controlButtons on a <clover-body> or <clover-canvas> replaces any of its controls with a button you make: a function given the control’s name, label and Clover’s icon, returning the button. Clover keeps it named, keyed and in its state, presses the control with it, and adds its popover. A <button> you write in a <clover-body> is one of its controls, after Clover’s, as a React host’s children are. class="clover-canvas-button" gives it Clover’s look, and a <clover-tip> in it the popover Clover’s controls show (<Clover.Tip> in React).

    <clover-body> <button type="button" class="clover-canvas-button" aria-label="Share"> <svg aria-hidden="true">…</svg> <clover-tip label="Share"></clover-tip> </button> </clover-body>

Helpers

A few of the functions Clover’s parts are built with, for parts of your own and the page around Clover, from @nulib/clover’s main entry. With React or with the elements, a bundler reaches them there; the elements’ CDN scripts do not carry them.

HelperWhat it does
readAs(format, value)How a text is read: "html", "markdown" or "plain", by its format’s media type, or, with none, by IIIF’s rule (HTML only from a first < to a last >).
toPlainText(html)IIIF HTML as plain text, its tags dropped and the common entities decoded, for where markup cannot go: an alt, an aria-label.
thumbnailSource(resource)Where a resource’s thumbnail comes from: its thumbnail, else the first image it paints, its placeholder’s or its accompanying Canvas’s. null if it has nothing to show.
regionSource(canvas, { x, y, w, h })A small picture of a region of a Canvas, in the Canvas’s own coordinates (an annotation’s xywh), cut by the image server. null where no image with a service is painted there.
resolveThumbnail(source, size, request)Either source as a URL about size pixels on its longer side (200 by default), asked of the image service as its own info.json allows: never larger than the image, and a level 0 server’s listed sizes only.
buildContentState({ manifestId, canvasId, region, time })Where a reader is, as a content state: { json, encoded }, the annotation and its base64url form for a link’s iiif-content. encodeContentState(json) encodes one of your own, decodeContentState(value) reads one.
import { buildContentState, resolveThumbnail, thumbnailSource, } from "@nulib/clover"; const { encoded } = buildContentState({ manifestId, canvasId, time: 42 }); const link = `https://example.org/viewer?iiif-content=${encoded}`; const picture = await resolveThumbnail(thumbnailSource(manifest), 400);

Types

Each component’s props are exported beside it (ViewerProps, RootProps, BodyProps…), with the types its props and hooks use, from @nulib/clover.

The module’s types declare every element (HTMLElementTagNameMap) and every event, so document.querySelector("clover-root") is typed, and what a script reaches: CloverRootApi, CloverCanvasHandle, CloverRenderer and CloverControlButtons. Its Custom Elements Manifest , @nulib/clover/web-components/custom-elements.json (the package’s customElements), gives each element’s attributes, properties and events to editors and tools.

What it loads

Only the parts you import, as your bundler splits them. Showing an image, a page with the Viewer in its Default template loads 94 kB to first view, its styles included, and one with a Canvas alone 43 kB of script. React is your page’s own, and not counted. Each part’s page gives what it adds to a page with a Root.

The script is 5 kB gzipped: it defines every element and draws none. An element’s code loads the first time one is on the page, with Preact and the styles of what it draws, so a page loads only the elements it uses, and their styles, to first view: 122 kB with a <clover-viewer>, the Viewer in its Default template among it (3.x’s element loads 563 kB), 128 kB with one in Document, 83 kB for a <clover-canvas> alone, and 33 kB for a <clover-root> and its <clover-label>s, as the package’s build measures them.

Everything else waits until it is needed: video streaming (hls.js), the sanitizer, an animation’s player, and the map — MapLibre, its worker and its stylesheet load the first time a map is shown.

Styles

Import @nulib/clover/styles.css once, and templates.css for a template other than Default. Their rules sit in @layer clover, so your own CSS wins; see Styling.

The element renders into its own children, with no shadow root, so the page’s CSS still reaches it, as 3.x’s does. Clover’s styles are added to the document once each, first in <head> and in the stylesheet’s order, so the page’s own rules win; see Styling.

Coming from Clover 3.x

Clover 3.xClover 4
Viewer<clover-viewer>Viewer<clover-viewer>, in its Default template
SliderItems<clover-items>
ImageCanvas<clover-canvas>, Clover’s own renderer
ScrollNot in Clover 4 yet; see Scroll in 3.x
Map<clover-map>Map<clover-map>, on MapLibre
Primitives (Label<clover-label>, Summary<clover-summary>…)Parts of the same names, reading from the nearest Root<clover-root>
options.optionsProps on the parts, and CSS custom properties
Information panelPanel<clover-panel>
Information panel pluginsClover.Tab<clover-tab>, and hooks such as useActiveCanvasroot.clover.activeCanvas()
customDisplaysrenderers.renderers

The Clover 3.x docs are kept beside these.