Documentation
Slider

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 — using Embla Carousel (opens in a new tab).

Collection

Note: Clover IIIF has discontinued use of Swiper (opens in a new tab) due to dependency security vulnerabilities and its impact on bundle size. We recommend upgrading to Clover IIIF 3.7.x or later, which now uses the lighter Embla Carousel (opens in a new tab) as a precaution.

Features

Provide a IIIF Presentation API (opens in a new tab) Collection and the component:

  • Renders a carousel style components
  • Renders label, summary for the referenced Collection
  • Transcribes homepage as a View All button referenced Collection
  • Renders items as a "slide" for each Manifest or Collection item
  • Renders label, summary for each item "slide"
  • Transcribes homepage as an <a> wrapping each item "slide"
  • Supports custom breakpoints to display customized slide counts
  • Supports callback onItemInteraction to 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 install @samvera/clover-iiif

Usage

React

Add the Slider component to your jsx or tsx code. The Slider uses Embla Carousel (opens in a new tab) and ships with the styles it needs, so no additional stylesheet imports are required.

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.

<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

PropTypeDefaultNotes
iiifContentstringURL of a IIIF resource to fetch. Optional when items is given.
itemsunknown[]An already-resolved Presentation API items array, rendered with no fetch. Wins over iiifContent.
behavior"individuals" | "paged" | "continuous" | "unordered"read from the resource, else individualsIIIF layout behavior. See Behavior.
labelInternationalStringthe resource's labelHeading text. Needed with items, where there is no resource to read it from.
summaryInternationalStringthe resource's summarySub-heading text.
onItemInteraction(item: SliderItem) => voidCalled instead of following the item's homepage link.
collectionId (deprecated)stringUse iiifContent.

Options

Passed together as an options object.

PropTypeDefaultNotes
options.breakpointsSliderBreakpointsPer-viewport spaceBetween and slidesPerGroup. See Custom Breakpoints.
options.credentials"omit" | "same-origin" | "include""omit"Passed to fetch when loading iiifContent.
options.customViewAllstringthe resource's homepageURL for the header's View All link.

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.

PropTypeDefaultNotes
searchbooleanfalseRender a filter control in the header.
onSearch(query: string) => voidReceives the query and takes the filtering over. Without it the Slider filters items itself, by label.
pagerSliderPagerA position in your own sequence. Shows a counter, and hands the arrows to onStep.
isRtlbooleanfalseRight-to-left control order, for a paged resource read that way.

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.

PropTypeDefaultNotes
renderItem(item: unknown, index: number) => ReactNodea linked figureDraw a slide's contents yourself. Called once per item, so a paged slide calls it twice.
activeIndexnumberIndex to keep centred. Changing it scrolls the track — this is how a host keeps its own selection in view.
showHeaderbooleantrueHide the label, summary and controls when the host supplies its own.
presentationalbooleanfalseDrop the carousel ARIA so the host can own the semantics. Required when the rail sits inside another widget role.
align"start" | "center" | "end""center"Where a scrolled-to slide comes to rest.
dragFreebooleanfalseScroll freely instead of settling on snap boundaries.
slidesToScrollnumber | "auto""auto"Slides per snap. 1 makes activeIndex address a single slide; "auto" pages by whatever fits.
wrapItems(items: ReactNode) => ReactNodeidentityWrap the slides, and only the slides, so you can own the region's semantics without enclosing the header.
⚠️

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 (opens in a new tab) 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" />
ValueLayout
individualsOne item per slide. The default.
pagedItems are paired into spreads, opening on a lone cover so later slides are recto/verso.
continuousOne item per slide with the gutter closed, so the sequence reads as a single unbroken object.
unorderedOne item per slide; the order carries no meaning.

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.

See Example Collection (opens in a new tab)

Header

Text

The top-level Collection label and summary (opens in a new tab) (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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) (if existing) data are mapped to a <figcaption>

"label": { "none": ["Pantalone classico"] }
"summary": { "none": ["Image"] }

Image

For each item, the thumbnail (opens in a new tab) 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 (opens in a new tab) 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"
  }
]