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
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.
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>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 itslabel, or a copy of itsslot="label"child, for markup.valueis the tab it opens on, and changing it shows another; the reader’s choice sendsclover-value-change. Withclosable, it has a close button, which sendsclover-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 writehiddenon the panel to start it shut, and hide or show it yourself whenever you like: the toggle follows. Each press sendsclover-open-change; cancel it to leave the panel as it is. - The Split’s elements are the panes and the bars themselves, as
Splitlays them out in React: one pane beside each handle has asize, 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.
renderValueon<clover-metadata>,renderEntryon<clover-annotations>andrenderItemon<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 ashandleonce their view is laid out, asonReadyhands it to a React host, and sendclover-ready:body.handle.zoomBy(2),body.handle.home(). Aclover-readyis the element’s own, and does not bubble, asloaddoes not. -
Renderers.
rendererson a<clover-root>,<clover-body>or<clover-viewer>draws views of your own, as React’s do. Each claims views bycanvasId,formatormatch, and itsrender(container, view)draws one into a box filling the Body and returns what undoes it.viewholds the view’s Canvases, their painting bodies and the root’sclover, 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.
controlButtonson a<clover-body>or<clover-canvas>replaces any of its controls with a button you make: a function given the control’sname,labeland Clover’sicon, 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.
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
The Clover 3.x docs are kept beside these.