Skip to Content

Customizing layouts

Fit a layout to your page: choose the Panel’s sections, set panes side by side, make a template, and add components of your own.

Panel

Choose what the Panel shows with Tab<clover-tab>s. Each one holds any component, Clover’s or your own, and Value<clover-value> shows text of your own, such as the citation here.

import * as Clover from "@nulib/clover"; <Clover.Root iiifContent="https://iiif.io/api/cookbook/recipe/0029-metadata-anywhere/manifest.json"> <Clover.Body style={{ height: "20rem" }} /> <div style={{ display: "flex", alignItems: "center", gap: "var(--clover-space-s)", paddingBlock: "var(--clover-space-s)", }} > <Clover.Label as="h3" style={{ flex: 1, margin: 0 }} /> <Clover.Previous /> <Clover.Next /> </div> <Clover.Panel> <Clover.Tab value="about" label="About"> <Clover.About label={false} /> </Clover.Tab> <Clover.Tab value="page" label="This page"> <Clover.Label of="canvas" as="h4" /> <Clover.Metadata of="canvas" /> </Clover.Tab> <Clover.Tab value="citation" label="Citation"> <Clover.Value as="p" format="text/markdown" value="Cite as: Glindoni, Henry Gillard. *John Dee performing an experiment before Queen Elizabeth I.* Oil on canvas, 1800–1899. Wellcome Collection, Wellcome Library no. 47369i." /> </Clover.Tab> </Clover.Panel> </Clover.Root>;
<script type="module"> import "@nulib/clover/web-components"; </script> <clover-root iiif-content="https://iiif.io/api/cookbook/recipe/0029-metadata-anywhere/manifest.json"> <clover-body style="height: 20rem"></clover-body> <div style=" display: flex; align-items: center; gap: var(--clover-space-s); padding-block: var(--clover-space-s); "> <clover-label as="h3" style="flex: 1; margin: 0"></clover-label> <clover-previous></clover-previous> <clover-next></clover-next> </div> <clover-panel> <clover-tab value="about" label="About"> <clover-about label="false"></clover-about> </clover-tab> <clover-tab value="page" label="This page"> <clover-label of="canvas" as="h4"></clover-label> <clover-metadata of="canvas"></clover-metadata> </clover-tab> <clover-tab value="citation" label="Citation"> <clover-value as="p" format="text/markdown" value="Cite as: Glindoni, Henry Gillard. *John Dee performing an experiment before Queen Elizabeth I.* Oil on canvas, 1800–1899. Wellcome Collection, Wellcome Library no. 47369i."></clover-value> </clover-tab> </clover-panel> </clover-root>

A Panel with nothing to show draws nothing. Ask usePanelSections first, and leave out the room around it:

import * as Clover from "@nulib/clover"; function MyPanel() { const sections = Clover.usePanelSections(["contents", "search"]); return sections.length ? <Clover.Panel sections={sections} /> : null; }

The Panel’s layouts, sections and props are on Panel.

Splitting panes

Set panes side by side with Split<clover-split>, and resize them with the SplitHandle<clover-split-handle> between them.

import * as Clover from "@nulib/clover"; <Clover.Root iiifContent="https://clover.iiif.library.northwestern.edu/mocks/paged/manifest.json"> <Clover.Split storageKey="my-layout" style={{ height: "28rem" }}> <Clover.SplitPane size="24rem" min="14rem" style={{ overflowY: "auto" }}> <Clover.Panel /> </Clover.SplitPane> <Clover.SplitHandle /> <Clover.SplitPane min="16rem"> <Clover.Body style={{ height: "100%" }} /> </Clover.SplitPane> </Clover.Split> </Clover.Root>;
<script type="module"> import "@nulib/clover/web-components"; </script> <clover-root iiif-content="https://clover.iiif.library.northwestern.edu/mocks/paged/manifest.json"> <clover-split storage-key="my-layout" style="height: 28rem"> <clover-split-pane size="24rem" min="14rem" style="overflow-y: auto"> <clover-panel></clover-panel> </clover-split-pane> <clover-split-handle></clover-split-handle> <clover-split-pane min="16rem"> <clover-body style="height: 100%"></clover-body> </clover-split-pane> </clover-split> </clover-root>

Long values

Value<clover-value> shows any text you hold, such as an annotation’s or your own.

Summary<clover-summary> cuts a long summary short under a “Read more” button, and ReadMore<clover-read-more> gives your own text the same.

.my-layout .clover-summary { --clover-summary-lines: 3; } .my-note { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
import * as Clover from "@nulib/clover"; <Clover.Summary /> <Clover.ReadMore> <p className="my-note">{note}</p> </Clover.ReadMore>
<script type="module"> import "@nulib/clover/web-components"; </script> <clover-summary></clover-summary> <clover-read-more> <p class="my-note">…</p> </clover-read-more>

Templates

Give the Viewer your layout as its template, as Clover’s templates are:

import Viewer, * as Clover from "@nulib/clover"; function MyLayout(props) { return ( <Clover.Root {...props}> <Clover.Body style={{ height: "24rem" }} /> <Clover.Items layout="pager" /> </Clover.Root> ); } <Viewer iiifContent="https://clover.iiif.library.northwestern.edu/mocks/paged/manifest.json" template={MyLayout} />;
<script type="module"> import "@nulib/clover/web-components"; </script> <!-- A layout of your own is the markup itself: a root and its elements. --> <clover-root iiif-content="https://clover.iiif.library.northwestern.edu/mocks/paged/manifest.json" > <clover-body style="height: 24rem"></clover-body> <clover-items layout="pager"></clover-items> </clover-root>

To start from one of Clover’s, see Change a template. What the templates share is on Helpers.

Creating custom components

Your components sit beside Clover’s and read the Root<clover-root> with hooks.

Headings

Draw a title one level below the heading it sits under. Clover.titled gives the right element for a headingLevelheading-level.

import * as Clover from "@nulib/clover"; function MyLayout({ iiifContent, headingLevel }) { const title = Clover.titled(headingLevel, "my-title"); return ( <Clover.Root iiifContent={iiifContent} {...title.group}> <Clover.Label as={title.as} {...title.title} /> <Clover.Body style={{ height: "20rem" }} /> </Clover.Root> ); } <MyLayout iiifContent="https://clover.iiif.library.northwestern.edu/mocks/paged/manifest.json" headingLevel={4} />;
<script type="module"> import "@nulib/clover/web-components"; </script> <clover-root iiif-content="https://clover.iiif.library.northwestern.edu/mocks/paged/manifest.json" > <clover-label as="h4"></clover-label> <clover-body style="height: 20rem"></clover-body> </clover-root>

Controls of your own

Name a control of your own with aria-label, and show that name on hover and focus with Tip<clover-tip>.

import { useState } from "react"; import * as Clover from "@nulib/clover"; function Bookmark() { const [saved, setSaved] = useState(false); return ( <button type="button" aria-label="Bookmark" aria-pressed={saved} onClick={() => setSaved(!saved)} style={{ position: "relative" }} > <svg aria-hidden="true" viewBox="0 0 24 24" width="24" height="24" fill={saved ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2" > <path d="M6 3h12v18l-6-4-6 4z" strokeLinejoin="round" /> </svg> <Clover.Tip label="Bookmark" /> </button> ); }
<button type="button" id="bookmark" aria-label="Bookmark" aria-pressed="false" style="position: relative" > <svg aria-hidden="true" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" > <path d="M6 3h12v18l-6-4-6 4z" stroke-linejoin="round" /> </svg> <clover-tip label="Bookmark"></clover-tip> </button> <script type="module"> import "@nulib/clover/web-components"; const bookmark = document.getElementById("bookmark"); bookmark.addEventListener("click", () => { const saved = bookmark.getAttribute("aria-pressed") !== "true"; bookmark.setAttribute("aria-pressed", String(saved)); bookmark .querySelector("svg") .setAttribute("fill", saved ? "currentColor" : "none"); }); </script>

Clover opens one popup at a time. Join yours to them with useOneOpenroot.clover.oneOpen():

import { useState } from "react"; import * as Clover from "@nulib/clover"; function useMyPopup() { const [open, setOpen] = useState(false); Clover.useOneOpen(open, () => setOpen(false)); return [open, setOpen]; }
<script type="module"> import "@nulib/clover/web-components"; const root = document.querySelector("clover-root"); const menu = document.getElementById("my-popup"); let release = null; const close = () => { menu.hidden = true; release?.(); }; const open = () => { menu.hidden = false; release = root.clover.oneOpen(close); }; </script>

Custom renderer

Draw a view your own way with renderers.renderers. Here a PDF viewer draws a Canvas that paints a PDF:

import { useState } from "react"; import * as Clover from "@nulib/clover"; import { Document, Page, pdfjs } from "react-pdf"; import "react-pdf/dist/Page/TextLayer.css"; // react-pdf stands in for the PDF viewer of your choice. pdfjs.GlobalWorkerOptions.workerSrc = new URL( "pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url, ).toString(); function PdfViewer({ bodies }) { // Inside the Root: Clover's hooks work here (useNavigation, useActiveCanvas, useClover…). const [pages, setPages] = useState(0); return ( <div style={{ position: "absolute", inset: 0, overflow: "auto" }} tabIndex={0} > <Document file={bodies[0].id} onLoadSuccess={({ numPages }) => setPages(numPages)} > {Array.from({ length: pages }, (_, index) => ( <Page key={index} pageNumber={index + 1} /> ))} </Document> </div> ); } <Clover.Root iiifContent="https://example.org/iiif/report/manifest.json" renderers={[{ format: ["application/pdf"], component: PdfViewer }]} > <Clover.Body /> </Clover.Root>;
<!-- A Manifest whose Canvas paints a PDF (application/pdf). --> <clover-root iiif-content="https://example.org/iiif/report/manifest.json"> <clover-body></clover-body> </clover-root> <script type="module"> import "@nulib/clover/web-components"; import { getDocument, GlobalWorkerOptions } from "pdfjs-dist"; import { EventBus, PDFViewer } from "pdfjs-dist/web/pdf_viewer.mjs"; // pdf.js's own viewer stands in for the PDF viewer of your choice. GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs"; document.querySelector("clover-root").renderers = [ { format: ["application/pdf"], render(container, { bodies }) { // pdf.js scrolls its pages in a box placed absolutely, and they go in .pdfViewer. const box = document.createElement("div"); box.style.cssText = "position: absolute; inset: 0; overflow: auto"; box.tabIndex = 0; box.innerHTML = '<div class="pdfViewer"></div>'; container.append(box); const viewer = new PDFViewer({ container: box, eventBus: new EventBus(), }); const loading = getDocument(bodies[0].id); loading.promise.then((pdf) => viewer.setDocument(pdf)); return () => loading.destroy(); }, }, ]; </script>

As web components, a renderer’s render draws the view into the box it is given.

A renderer gets the view’s canvases.canvases and their painting bodies.

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

Helpers

Clover’s helpers read IIIF resources for your components: thumbnails, text, views, places and content states. See Helpers.

Languages

Clover’s strings, such as “About” and “Zoom in”, follow your page’s <html lang>, or the language you set with initCloverI18n. It ships English, Finnish, French, Norwegian (Bokmål and Nynorsk), Portuguese (European) and Spanish.

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>

Languages other than English load when first needed. On a server or in a test, wait for them:

import { initCloverI18n } from "@nulib/clover"; await initCloverI18n({ lng: "fr" }).ready();

In a component of your own, useLangOf gives each string its lang:

import * as Clover from "@nulib/clover"; function CloseButton({ onClick }) { const t = Clover.useT(); const langOf = Clover.useLangOf(); return ( <button type="button" aria-label={t("commonClose")} lang={langOf("commonClose")} onClick={onClick} style={{ position: "relative" }} > <svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"> <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="2" /> </svg> <Clover.Tip label={t("commonClose")} /> </button> ); }
<button type="button" id="close" style="position: relative"> <svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"> <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="2" /> </svg> <clover-tip></clover-tip> </button> <script type="module"> import "@nulib/clover/web-components"; 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>

Styles

Clover’s stylesheets, custom properties and classes are on Styling.