Skip to Content

Composing layouts

Every template is a composition of Clover’s parts, and so is a layout of your own. A Root<clover-root> loads the content and holds where the reader is. Every part inside it reads from the nearest Root<clover-root>, wherever it sits in your markup, so a layout is your own HTML with parts placed in it.

A layout of your own

The parts are named exports. import * as Clover gives them dot notation, and they tree-shake: a page loads only the parts it renders. Here the title, position and buttons sit beside the Body. The code is live: move the aside’s parts, or change what a Label reads, and the layout follows.

JSX
import * as Clover from "@nulib/clover";import "@nulib/clover/styles.css"; <Clover.RootiiifContent="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json"className="my-layout"><Clover.Body /><aside><Clover.Label as= /><Clover.Label of= /><Clover.Position /><Clover.Previous /><Clover.Next /></aside></Clover.Root>;
HTML
<script type="module">import "@nulib/clover/web-components";</script> <clover-rootiiif-content="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json"class="my-layout"><clover-body></clover-body><aside><clover-label as=></clover-label><clover-label of=></clover-label><clover-position></clover-position><clover-previous></clover-previous><clover-next></clover-next></aside></clover-root>
Arrow up or down moves the part, as Move up and Move down do.

Start from a template

The templates are built only from these parts, so a template’s source is a good place to start. Copy Default.tsx and Default.css from packages/clover/src/templates/default/ (or another template’s folder) into your app, and read ../../parts in its imports as @nulib/clover. Rename its clover-template-default-* classes to your own, and change what you like: nothing in a template is private.

To keep the Viewer’s shell, pass your layout as its template; it takes the Root<clover-root> props like any template:

import Viewer, * as Clover from "@nulib/clover"; function MyLayout(props) { return ( <Clover.Root {...props}> <Clover.Body /> <Clover.Items layout="pager" /> </Clover.Root> ); } <Viewer iiifContent={url} template={MyLayout} />;
<!-- A layout of your own is the markup itself: a root and its parts. --> <clover-root iiif-content="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json" > <clover-body></clover-body> <clover-items layout="pager"></clover-items> </clover-root>

Root

PropTypeDescription
iiifContentiiif-contentstring | objectA URL, a IIIF resource (Presentation 2, 3 or 4), or a content state.
canvasIdcanvas-idstringThe Canvas to show, controlled. Moving calls onCanvasChangeclover-canvas-change instead.
defaultCanvasIdcanvas-idstringThe Canvas to start on. A content state, then the Manifest’s start, win over it.
onCanvasChangeclover-canvas-change(canvasId: string) => voidThe reader moved to a Canvas.
onStatusChangeclover-status-change(status, error) => void"loading", "ready" or "error".
request.request{ credentials, headers, fetch }Used for every request any part makes.
languagestringThe language to read IIIF text in. The page’s, then the reader’s, by default.
annotationsURL, object or listAnnotationPages or AnnotationCollections to show beside the Manifest’s own.
manifestIdmanifest-idstringIn a Collection, the member to show, controlled.
defaultManifestIdmanifest-idstringIn a Collection, the member to open first.
onManifestChangeclover-manifest-change(manifestId) => voidIn a Collection, the reader moved, or asks to move, to another member.
searchQuerysearch-querystringA Content Search to run once the Manifest is shown, and whenever it changes.
onContentStateChangeclover-content-state-change({ json, encoded }) => voidWhere the reader is, as a content state, each time it settles.
renderers.rendererslistComponents that draw views Clover does not; see below.
aselement typeDefault div.

iiifContentiiif-content can be a Manifest, a Collection, a Canvas or a content state, as a URL or an object; see what the Viewer opens. Every template, and the Viewer, takes these props and hands them to its Root<clover-root>.

Parts

PartWhat it renders
Root<clover-root>Loads the content and holds where the reader is. Renders a div (or as), and announces moves.
Body<clover-body>The current view, drawn by Canvas<clover-canvas>: a page, a spread, a video or sound. Children join its controls. While it loads, Clover’s loading mark runs along its top edge; nothing moves.
Label<clover-label>The Manifest’s label, or the current Canvas’s with of="canvas", in the reader’s language.
Previous<clover-previous>, Next<clover-next>Round arrow controls that move a view at a time (a spread, in a paged book), as Items’ arrows do, or with of="members" a Collection’s Manifest. Each names itself on hover and focus, and shows disabled at an end.
Position<clover-position>“2 / 5”, counted in views (or members); “Item 2 of 5” to assistive technology, in the reader’s language. Its room is its widest count’s.
Status<clover-status>Loading, as Clover’s loading mark (announced “Loading…”), or what went wrong. Once loaded, its status region stays, empty and hidden, so the next load is announced. loading={false}loading="false" only announces it, where Body<clover-body> shows it.
Items<clover-items>Thumbnails of the views, as a slider, list or grid, or layout="pager": Position<clover-position>, Previous<clover-previous> and Next<clover-next> in a nav. See Items.
Panel<clover-panel>The resource’s information in sections, as tabs or an accordion: the ones it has data for, or Tab<clover-tab>s of your own. See Panel.
Tab<clover-tab>One tab of the panel: value, label, and what it shows. Your own components go here.
About<clover-about>Everything the Manifest says about itself, as 3.x’s About tab shows it.
PanelToggle<clover-panel-toggle>Shows and hides the panel. As a child of Body<clover-body>, it sits with the Canvas’s controls.
Annotations<clover-annotations>The view’s annotations by set: content to read, a button to find each on the Canvas, and an eye before each set’s label that hides its annotations on the Canvas (shared with the Canvas’s annotations menu).
Download<clover-download>The rendering of the page in view and of the Manifest (a PDF, say), as links in two groups.
Transcript<clover-transcript>The view’s captions as text: the current cue marked as it plays, a press seeking there.
Contents<clover-contents>The table of contents from the Manifest’s Ranges, the entry the reader is in marked. Hovering an entry draws its chapter out on the Canvas’s seek bar, and the chapter under the pointer there marks its entry.
Search<clover-search>IIIF Content Search (1 or 2): a field, then the hits by page with the words around them.
Collection<clover-collection>The Collection the Manifest is in: its label, summary, members, homepage and address, as the Panel<clover-panel>’s Collection section shows it. Fetched when it first shows. headingLevelheading-level sets its headings (default 3).
CollectionSelect<clover-collection-select>In a Collection, a menu of its members; choosing one shows it. By keyboard, the arrows move through the members and Enter opens one; Escape, or leaving the menu, takes the step back.
Canvas<clover-canvas>The renderer itself. Inside a Root<clover-root> it shows the current view; see Canvas.
Tip<clover-tip>A control’s popover: its name and its key, on hover and focus. See below.

Parts that have nothing to show render nothing: Items<clover-items> and its pager for a single view, Label<clover-label> with no label.

Properties

Primitives has a page for each. Each IIIF descriptive property is a part of its own, reading the Manifest, or the current Canvas with of="canvas". A value passed as value wins, so they also work outside a Root<clover-root>. Text is in the reader’s language and marked with lang; HTML is limited to what IIIF allows and sanitized, by a sanitizer that loads only when a value carries markup.

PartProperty
Label<clover-label>label (takes label rather than value)
Summary<clover-summary>summary, cut to five lines with “Read more” (below)
Metadata<clover-metadata>metadata, as a description list; renderValue.renderValue to customize
RequiredStatement<clover-required-statement>requiredStatement
Rights<clover-rights>rights, as a link
Homepage<clover-homepage>, Rendering<clover-rendering>, SeeAlso<clover-see-also>, PartOf<clover-part-of>Linked resources, by label, with their format
Thumbnail<clover-thumbnail>thumbnail (or a Canvas’s first image), at size
Id<clover-id>The resource’s id, as a link

Value<clover-value> shows any other IIIF language map you hold, an annotation’s text say, marked and sanitized the same way. It reads a value strictly by its format: text/html as IIIF’s HTML, sanitized to the tags the Presentation API allows; text/plain as written, markup and all, its line breaks kept; and with no format, by IIIF’s rule for property values, as HTML only when it runs from a first < to a last >. With format="text/markdown" it reads Markdown, as Annotations<clover-annotations> does for a text/markdown body: its lists are lists, and its headings are headings to assistive technology (role="heading", drawn bold, so your page’s heading styles stay out of them). The first heading takes headingLevelheading-level (default 3), and each later one sits a level under the nearest shallower heading before it, so none is skipped even where the source skips one. In Annotations<clover-annotations> they sit a level under the set’s heading. Past h6, none is a heading (Headings).

Every summary Clover shows is Summary<clover-summary>, cut to five lines with a “Read more” button under it when it runs longer, and “Read less” to cut it again. The number of lines is a CSS custom property, --clover-summary-lines, set on .clover-summary or anything around it; none shows it whole:

.my-layout .clover-summary { --clover-summary-lines: 3; }

ReadMore<clover-read-more> gives text of your own the same button: wrap one element (or a component that forwards its ref) and clamp it in your CSS; it carries data-read-more="open" while open.

<Clover.ReadMore> <p className="my-note">{note}</p> </Clover.ReadMore>
<clover-read-more> <p class="my-note">…</p> </clover-read-more>

The information panel

Panel<clover-panel> shows About, and each other section the content has something for: Collection, Contents, Search, Map, Transcript and Annotations. Give it Tab<clover-tab>s to choose the sections yourself, in your order. Each part a default section uses works on its own too, in a Tab<clover-tab> or anywhere in the layout, and so does a part of your own.

<Clover.Panel> <Clover.Tab value="about" label="About"> <Clover.About /> </Clover.Tab> <Clover.Tab value="page" label="This page"> <Clover.Label of="canvas" as="h3" /> <Clover.Summary of="canvas" /> <Clover.Metadata of="canvas" /> </Clover.Tab> </Clover.Panel>
<clover-panel> <clover-tab value="about" label="About"> <clover-about></clover-about> </clover-tab> <clover-tab value="page" label="This page"> <clover-label of="canvas" as="h3"></clover-label> <clover-summary of="canvas"></clover-summary> <clover-metadata of="canvas"></clover-metadata> </clover-tab> </clover-panel>

PanelToggle<clover-panel-toggle> shows and hides it; as a child of Body<clover-body>, it sits with the Canvas’s controls. Panel has its layouts, the sections and when each shows, headings, closing a panel that floats over the page, and its props.

Panes side by side

Split<clover-split>, SplitPane<clover-split-pane> and SplitHandle<clover-split-handle> put panes side by side, with a bar between them that shares the width. One pane beside each bar has a size (with min and max, in px, rem or %); the other takes what is left. The reader drags the bar by its grip, or clicks or taps it: each click steps the pane on to its next width, from its narrowest to its own size to its widest and then around again, so one pointer can resize it without dragging. The bar is a focusable separator that names itself on hover and focus: the arrow keys move it (Shift for bigger steps), and Home and End go to the limits. With storageKeystorage-key the reader’s sizes are remembered. The Default and Document templates use it, and nothing ties it to any part.

<Clover.Root iiifContent={url}> <Clover.Split storageKey="my-layout"> <Clover.SplitPane min="16rem"> <Clover.Body /> </Clover.SplitPane> <Clover.SplitHandle /> <Clover.SplitPane size="24rem" min="14rem"> <Clover.Panel /> </Clover.SplitPane> </Clover.Split> </Clover.Root>
<clover-root iiif-content="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json"> <clover-split storage-key="my-layout"> <clover-split-pane min="16rem"> <clover-body></clover-body> </clover-split-pane> <clover-split-handle></clover-split-handle> <clover-split-pane size="24rem" min="14rem"> <clover-panel></clover-panel> </clover-split-pane> </clover-split> </clover-root>

Parts of your own

Hooks read from the nearest Root<clover-root>. Each re-renders only when what it reads changes.

HookReturns
useStatus()root.clover.status(){ status, error }
useManifest()root.clover.manifest()The Manifest, in Presentation 4.
useSequence()root.clover.sequence(){ canvases, views, direction }
useVisibleCanvases()root.clover.visibleCanvases()The Canvases of the current view.
useActiveCanvas()root.clover.activeCanvas()The Canvas the reader is on.
useNavigation()root.clover.navigation(){ position, total, hasPrevious, hasNext, previous, next, first, last, goTo }
useLocalized(value){ text, lang } for a IIIF language map, in the Root’s language.
useT()t(key, values): Clover’s strings, in its interface language.
useLangOf()langOf(key): a string’s lang where it is not the page’s language.
function PageCount() { const { position, total } = Clover.useNavigation(); return ( <span> {position + 1} / {total} </span> ); }
<span id="page-count"></span> <script type="module"> const root = document.querySelector("clover-root"); const count = document.getElementById("page-count"); const show = () => { const { position, total } = root.clover.navigation(); count.textContent = `${position + 1} / ${total}`; }; const start = () => { show(); root.clover.subscribe(show); }; if (root.clover) start(); else root.addEventListener("clover-ready", start, { once: true }); </script>

useClover()root.clover gives { clover }, the Root’s state and actions (clover.goTo(canvasId), clover.next(), clover.search(query) and the rest), for parts that act rather than read.

Headings

Every part that draws a title takes headingLevelheading-level, one below the heading it sits under, so a screen reader’s list of headings follows your page. HTML’s headings end at h6, and screen readers misread a deeper aria-level (JAWS, and TalkBack with Chrome, announce a 7 as a 2), so past 6 Clover draws no heading: a title that would be an h7 names the group it heads instead. Clover.titled does the same in a layout of your own.

function MyLayout({ url, headingLevel }) { const title = Clover.titled(headingLevel, "my-title"); return ( <Clover.Root iiifContent={url} {...title.group}> <Clover.Label as={title.as} {...title.title} /> <Clover.Body /> </Clover.Root> ); }
<clover-root iiif-content="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json" > <clover-label as="h2"></clover-label> <clover-body></clover-body> </clover-root>

Controls of your own

Every control Clover draws as an icon alone names itself in a popover on hover and on keyboard focus, with its keyboard shortcut when it has one. Give a control of your own the same: its name as aria-label, its key (if it has one) as aria-keyshortcuts, and Tip<clover-tip> inside it, last, with the same words. Tip is hidden from assistive technology, which reads the control’s own name, and hides on Escape. Each time it shows, it is kept inside whatever clips it (the Body, a scrolling panel, the window): it wraps to the room there is and moves in from the edge.

<button type="button" aria-label="Bookmark" onClick={save}> <BookmarkIcon aria-hidden="true" /> <Clover.Tip label="Bookmark" /> </button>
<button type="button" id="bookmark" aria-label="Bookmark"> <svg aria-hidden="true">…</svg> <clover-tip label="Bookmark"></clover-tip> </button> <script type="module"> document.getElementById("bookmark").addEventListener("click", save); </script>

Clover’s popups open one at a time: the Canvas’s information and its menus (annotations, Choice, an animation’s frames), the playback controls’ menus and Download’s list. Opening one closes whichever other is open, however it was opened. A popup of your own joins them with useOneOpenroot.clover.oneOpen(), given its open state and the way to close it:

const [open, setOpen] = useState(false); Clover.useOneOpen(open, () => setOpen(false));
<script type="module"> const root = document.querySelector("clover-root"); let release = null; const close = () => { menu.hidden = true; release?.(); }; const open = () => { menu.hidden = false; release = root.clover.oneOpen(close); }; </script>

Languages

Clover’s own words (its controls’ names, its messages, what it announces) are in its interface language: the one set with initCloverI18n, else the page’s <html lang>, else the first of the browser’s languages it has, else English, so it speaks the language of the page around it. It ships English, Finnish, French, Norwegian (Bokmål and Nynorsk), Portuguese (European) and Spanish, each complete. IIIF text is read in the Root’s language, else the page’s, else the browser’s. Where what the resource says of itself comes in several languages, the Panel’s About offers them, and the one the reader chooses is the whole Root’s until the page sets another language.

import { initCloverI18n } from "@nulib/clover"; // The page's language, and strings of your own for one Clover does not ship. initCloverI18n({ lng: "de", resources: { de: { clover: { informationPanelTabsAbout: "Über" } } }, });
<script type="module"> import { initCloverI18n } from "@nulib/clover/web-components"; initCloverI18n({ lng: "de", resources: { de: { clover: { informationPanelTabsAbout: "Über" } } }, }); </script>

English comes with Clover; each other language loads the first time Clover is to speak it, so a page in English never fetches one. Until it arrives, Clover’s words show in English, marked lang="en", and they change when it does. A page rendered on the server, or a test, that needs them in place first waits for them:

await initCloverI18n({ lng: "fr" }).ready();

Where the interface language is not the page’s (a French reader on an English page), each of Clover’s strings carries it as lang, on the element that shows it or is named by it. The page’s language is the nearest lang around the Root<clover-root>, or one you give the Root<clover-root>. The Root<clover-root> carries none of its own, so what you put in it, and IIIF text in no language (none), keep the page’s. A language you bootstrap with some strings falls back to English for the rest, each such string marked lang="en" too. In a part of your own, mark Clover’s strings the same way:

const t = Clover.useT(); const langOf = Clover.useLangOf(); <button type="button" aria-label={t("commonClose")} lang={langOf("commonClose")} > <CloseIcon aria-hidden="true" /> <Clover.Tip label={t("commonClose")} /> </button>;
<button type="button" id="close"> <svg aria-hidden="true">…</svg> <clover-tip></clover-tip> </button> <script type="module"> const root = document.querySelector("clover-root"); const close = document.getElementById("close"); const start = () => { const label = root.clover.t("commonClose"); const lang = root.clover.langOf("commonClose"); close.setAttribute("aria-label", label); close.querySelector("clover-tip").setAttribute("label", label); if (lang) close.lang = lang; else close.removeAttribute("lang"); }; if (root.clover) start(); else root.addEventListener("clover-ready", start, { once: true }); </script>

Renderers of your own

Give Root<clover-root> (or one Body<clover-body>) renderers.renderers, and a view they claim is drawn by your component instead of Clover’s own (Canvas, Timeline or Scene): a PDF, a 3D model drawn your way, anything Clover does not draw, or draws otherwise than you want. Claim a view by its Canvas id, by a painted body’s format, or with a match test. The first renderer that claims a view draws it. This is 3.x’s customDisplays.

function ModelViewer({ bodies }) { // Inside the Root: Clover's hooks work here (useNavigation, useActiveCanvas, useClover…). return <model-viewer src={bodies[0].id} camera-controls />; } <Clover.Root iiifContent={url} renderers={[{ format: ["model/gltf-binary"], component: ModelViewer }]} > <Clover.Body /> </Clover.Root>;
<clover-root iiif-content="https://iiif.io/api/cookbook/recipe/0608-mvm-3d/manifest.json" > <clover-body></clover-body> </clover-root> <script type="module"> document.querySelector("clover-root").renderers = [ { format: ["model/gltf-binary"], render(container, { bodies }) { const model = document.createElement("model-viewer"); model.setAttribute("src", bodies[0].id); model.setAttribute("camera-controls", ""); container.append(model); }, }, ]; </script>

A renderer gets the view’s canvases.canvases, their painting bodies (a Choice as its first item, a SpecificResource as its source) and Body<clover-body>’s children. The Viewer and every template pass renderers.renderers through to their Root<clover-root>.

3.x customDisplays4.x renderers.renderers
target.canvasIdcanvasIdcanvas-id
target.paintingFormatformat
(none)match: ({ canvases, bodies }) => boolean
display.componentcomponent
display.componentPropsclose over them: component: (p) => <X {...p} a />
id, annotationBody propscanvases.canvases, bodies props
hooks propClover’s hooks, called in the component

Styles

Clover’s stylesheets, the custom properties a layout sets (the Body’s height, the insets, the colors), the classes and states to select, and the color pairs to keep accessible are on Styling.