@samvera/clover-iiif, the earlier version of Clover, kept here for reference. These docs lead with Clover 4: Get started.Slider
A UI component that renders an item carousel for the contents of a IIIF Collection — or for any Presentation API items array you hand it — on the browser’s own scrolling.
Note: The Slider no longer uses a carousel library. It moved from Swiper to Embla Carousel in
3.7.x, and from Embla to native scrolling with CSS scroll snapping in4.0.
Features
Provide a IIIF Presentation API Collection and the component:
- Renders a carousel style components
- Renders
label,summaryfor the referenced Collection - Transcribes
homepageas a View All button referenced Collection - Renders items as a “slide” for each Manifest or Collection item
- Renders
label,summaryfor each item “slide” - Transcribes
homepageas an<a>wrapping each item “slide” - Supports custom breakpoints to display customized slide counts
- Supports callback
onItemInteractionto override default “slide” behavior - Doubles as a rail inside another component — the
Viewer’s canvas navigation is this component. See Two Ways to Use It
Installation
npm
npm install @samvera/clover-iiifUsage
React
Add the Slider component to your jsx or tsx code. The Slider scrolls natively, with CSS scroll snapping, and ships with the styles it needs, so no additional stylesheet imports are required. Wheel, trackpad, touch and keyboard scrolling work as on any page, and a mouse can drag the strip.
import Slider from "@samvera/clover-iiif/slider";Render the slider with IIIF Collection URI. The only required prop is the iiifContent, which is the URI of the IIIF Collection.
Code
<Slider iiifContent="https://api.dc.library.northwestern.edu/api/v2/collections/c373ecd2-2c45-45f2-9f9e-52dc244870bd?as=iiif" />Two Ways to Use It
Slider is one carousel serving two jobs. Which one you get depends on whether you let it
draw its own slides or hand it something else to draw.
A carousel of links
The default. Give it a Collection and each slide becomes an <a> whose href is that item’s
homepage[0].id — so a click leaves for wherever the resource says it lives. An item without a
homepage falls back to #, and the Collection’s own homepage becomes the header’s View
All link.
<Slider iiifContent="https://example.org/collection.json" />To route a click yourself instead of following the link, pass onItemInteraction. It receives
the whole item and suppresses the navigation, which is what you want inside a single-page app
that has its own router. See Custom Interactions.
<Slider
iiifContent="https://example.org/collection.json"
onItemInteraction={(item) =>
router.push(`/items/${encodeURIComponent(item.id)}`)
}
/>A rail inside another component
The Viewer’s canvas navigation is this component. Nothing about it is Viewer-specific: the
carousel, its arrows, its filter and its counter are the Slider’s, and what the Viewer supplies
is the part only it can know — which canvases exist, what a step means, and what a slide should
look like.
<Slider
/* draw my slides, not yours */
renderItem={(group) => <Thumbnail group={group} />}
items={groups}
/* keep my selection centred; one slide per snap so the index addresses a slide */
activeIndex={activeGroupIndex}
slidesToScroll={1}
/* the arrows step my sequence and show a counter, rather than scrolling the rail */
pager={{
current: canvasIndex + 1,
total: canvases.length,
onStep: stepCanvas,
}}
/* I own the filtering, because only I can tell what a query matches */
search
onSearch={setFilter}
/* I own the region's semantics: a radio group around the slides, not the header */
presentational
wrapItems={(slides) => <RadioGroup.Root>{slides}</RadioGroup.Root>}
/>The distinction that matters is renderItem. Supply it and the anchor-and-figure slide is
gone, along with the homepage link — a slide is whatever you return, and clicks are yours to
handle. Leave it out and you are in carousel mode.
Each of those props is described under Controls and Embedding.
API Reference
Give Slider either a resource to fetch or a list of items to render. Everything else is optional.
Content
Options
Passed together as an options object.
Controls
The header’s prev/next buttons are always there. A counter and a filter are opt-in, and the Viewer turns both on — its canvas rail is this same header, not a second implementation of one.
Left to itself, search narrows the Slider’s own items by label. Pass onSearch when only you can tell what a query matches — the Viewer’s slides are paged groups of canvas indices with no label of their own, so it filters and hands back a shorter list. Either way the carousel just re-measures on the new count.
interface SliderPager {
current: number; // 1-based, matching what the counter shows
total: number;
onStep: (step: -1 | 1) => void;
}pager changes what the arrows do. Without one they scroll the rail, which is what a Collection carousel wants. With one they step your sequence and the rail follows the selection through activeIndex — the Viewer’s arrows change the canvas on show rather than sliding the thumbnails out from under it. Availability follows from current: the back arrow is dead at 1, the forward arrow at total.
The counter is free to count in different units than the slides. The Viewer counts canvases while its slides are paged spreads, because a reader of a manuscript is on a page, not on a spread.
Embedding
Seams for using Slider as a subcomponent, the way the Viewer’s canvas rail does. Skip these for a standalone carousel.
presentational is not cosmetic. Nesting the carousel’s role="group" slides
inside another widget role is invalid, and enough to stop a radio group
handing out its single roving tab stop — which leaves the rail unreachable by
keyboard.
Sizing
Slides are sized by their own content, and the track is the sum of their widths — so how many are visible follows from the viewport rather than being divided into it. There is deliberately no slidesPerView. Set the size in CSS:
body {
--clover-thumbnail-width: 12rem; /* the Slider defaults to 15rem */
}Cards are square, sized from that width, unless a height is set:
body {
--clover-thumbnail-width: 20rem;
--clover-thumbnail-height: 12rem;
}These are the same two properties the Viewer’s canvas thumbnails read, so one declaration scales every thumbnail in the library at once. The Slider only differs in what it falls back to when neither is set — 15rem, against the rail’s 161.8px, because a carousel standing on its own wants a larger card than a strip tucked under a viewer. See Thumbnail size.
The prev/next controls advance by whatever currently fits the viewport, and centre it. That follows from slides being content-sized: a fixed count cannot know how many are on screen, so it would page past what the reader can see.
Custom Breakpoints
Pass a breakpoints object to change the gutter or pin the paging, where each key is a min-width pixel value and each value applies at or above that width.
spaceBetween takes any CSS length — a bare number is treated as pixels, which is how this option has always behaved. slidesPerGroup pins the advance to a fixed number of slides instead of a viewportful, which is only useful if you have also fixed the card width.
const MyCustomSlider = () => {
const iiifContent =
"https://api.dc.library.northwestern.edu/api/v2/collections/c373ecd2-2c45-45f2-9f9e-52dc244870bd?as=iiif";
const customBreakpoints = {
320: {
slidesPerGroup: 2,
spaceBetween: 20,
},
480: {
slidesPerGroup: 3,
spaceBetween: 30,
},
640: {
slidesPerGroup: 4,
spaceBetween: 40,
},
};
return (
<Slider
iiifContent={iiifContent}
options={{
breakpoints: customBreakpoints,
}}
/>
);
};Behavior
Slider lays items out according to the IIIF behavior declared by the resource it opens, and falls back to individuals — the specification’s own default — when none is declared.
Pass behavior to override it. This is also how you set the layout when handing the component an items array directly, since there is no resource to read it from:
<Slider iiifContent="https://example.org/collection.json" behavior="paged" />Only the specification’s layout behaviors are accepted. facing-pages and non-paged describe a single Canvas rather than the sequence, and the temporal, collection and range behaviors say nothing about layout, so all are ignored here.
Custom Interactions
The default behavior for a click (or press) event on each of the individual items is to route to the href value set by the IIIF Presentation 3.0 API homepage[0].id for each item entry.
You can optionally set an event handler for the onItemInteraction value as a callback for a custom action. The full item object will be passed back to the consuming application.
const MyCustomSlider = () => {
const iiifContent =
"https://api.dc.library.northwestern.edu/api/v2/collections/c373ecd2-2c45-45f2-9f9e-52dc244870bd?as=iiif";
const handleItemInteraction = (item: Manifest | Collection) => {
// do something with `item`
console.log(item);
};
return (
<Slider
iiifContent={iiifContent}
onItemInteraction={handleItemInteraction}
/>
);
};The returned value of item provides the entire object for your custom interaction.
{
"id": "https://api.dc.library.northwestern.edu/api/v2/works/2de0355c-8e48-4478-93af-8cbd1437bd16?as=iiif",
"type": "Manifest",
"homepage": [
{
"id": "https://dc.library.northwestern.edu/items/2de0355c-8e48-4478-93af-8cbd1437bd16",
"type": "Text",
"format": "text/html",
"label": {
"none": ["Pulcinella \"tiepolano\""]
}
}
],
"label": {
"none": ["Pulcinella \"tiepolano\""]
},
"summary": {
"none": ["Image"]
},
"thumbnail": [
{
"id": "https://api.dc.library.northwestern.edu/api/v2/works/2de0355c-8e48-4478-93af-8cbd1437bd16/thumbnail",
"format": "image/jpeg",
"type": "Image",
"width": 400,
"height": 400
}
]
}Collection Anatomy
Bloom accepts both Presentation API 2.x and Presentation API 3.0 Collections.
Header
Text
The top-level Collection label and summary (if existing) data is mapped to a Header sub-component
"label": {
"none": ["Commedia dell'Arte: The Masks of Antonio Fava"]
}"summary": {
"none": [
"The Commedia dell'Arte, the famous improvisational theatre style born in Renaissance Italy, remains a major influence in today's theatre. Antonio Fava is an actor, comedian, author, director, musician, mask maker and Internationally renowned Maestro of Commedia dell'Arte."
]
}Link
The top-level Collection homepage represents the href attribute wrapping an HTML <a> element on the Header label
"homepage": [
{
"id": "https://dc.library.northwestern.edu/collections/c373ecd2-2c45-45f2-9f9e-52dc244870bd",
"type": "Text",
"label": { "none": ["Commedia dell'Arte: The Masks of Antonio Fava"] },
"format": "text/html"
}
]Items
The items array is mapped to the slider/carousel with each item rendering as an HTML <figure>
"items": [
{
"id": "https://iiif.stack.rdc.library.northwestern.edu/public/72/98/fd/ce/-a/dc/1-/45/01/-9/e1/4-/9e/8b/d9/85/e1/49-manifest.json",
"type": "Manifest",
"label": { "none": ["Pantalone classico"] },
"summary": { "none": ["Image"] },
"thumbnail": [
{
"id": "https://iiif.stack.rdc.library.northwestern.edu/iiif/2/180682c9-dfaf-4881-b7b6-1f2f21092d4f/full/200,/0/default.jpg",
"type": "Image",
"format": "image/jpeg",
"service": [
{
"id": "https://iiif.stack.rdc.library.northwestern.edu/iiif/2/180682c9-dfaf-4881-b7b6-1f2f21092d4f",
"profile": "http://iiif.io/api/image/2/level2.json",
"type": "ImageService2"
}
],
"width": 200,
"height": 200
}
],
"homepage": [
{
"id": "https://dc.library.northwestern.edu/items/7298fdce-adc1-4501-9e14-9e8bd985e149",
"type": "Text",
"label": { "none": ["Pantalone classico"] },
"format": "text/html"
}
]
}
]Text
For each item, the label and summary (if existing) data are mapped to a <figcaption>
"label": { "none": ["Pantalone classico"] }"summary": { "none": ["Image"] }Image
For each item, the thumbnail is rendered as an <img /> element within the <figure>
"thumbnail": [
{
"id": "https://iiif.stack.rdc.library.northwestern.edu/iiif/2/180682c9-dfaf-4881-b7b6-1f2f21092d4f/full/200,/0/default.jpg",
"type": "Image",
"format": "image/jpeg",
"service": [
{
"id": "https://iiif.stack.rdc.library.northwestern.edu/iiif/2/180682c9-dfaf-4881-b7b6-1f2f21092d4f",
"profile": "http://iiif.io/api/image/2/level2.json",
"type": "ImageService2"
}
],
"width": 200,
"height": 200
}
]Link
The homepage for each item represents the href attribute of the HTML <a> element wrapping <figure>
"homepage": [
{
"id": "https://dc.library.northwestern.edu/items/7298fdce-adc1-4501-9e14-9e8bd985e149",
"type": "Text",
"label": { "none": ["Pantalone classico"] },
"format": "text/html"
}
]