Skip to content

Getting Started

Terminal
npm install @rpxl/riffle

Give your page an empty container, <div id="stack"></div>, then run this script:

quickstart.ts
import { createRiffle } from '@rpxl/riffle'
const stack = document.getElementById('stack')!
// One grid cell for every card: Riffle positions them from there with transforms.
// justify-content keeps that cell the card's own width (centred), so the fan
// scales about the card itself.
stack.style.cssText = 'display: grid; justify-content: center; padding: 32px 0'
// Name the carousel: screen readers announce this label with it.
stack.setAttribute('aria-label', 'Films')
const riffle = createRiffle(stack, { count: 5, cardWidth: 300, cardHeight: 400 })
for (let i = 0; i < 5; i++) {
const card = stack.appendChild(document.createElement('div'))
card.textContent = String(i + 1)
card.style.cssText = `grid-area: 1 / 1; width: 300px; height: 400px; border-radius: 16px;
background: hsl(${i * 72} 65% 45%); color: white; display: grid; place-items: center`
riffle.registerNode(i, card)
}
// Drag the front card, or wire riffle.next(), riffle.prev() and riffle.goTo(index)
// to controls of your own.

That exact file, running. Drag the front card:

  • The container is a single grid cell. Every card sits in it (grid-area: 1 / 1), and the engine moves each one from there with transforms.
  • createRiffle(stack, { count, cardWidth, cardHeight }) creates the engine and marks the container up as a carousel, with a live region and keyboard support.
  • registerNode(i, card) tells the engine which element is card i.
  • cardWidth and cardHeight describe each card’s geometry to the engine (how far a drag travels, how the fan is laid out) and do not size or style any element: the cards’ own styles give them their size, background and radius.
  • The engine marks the stack as a carousel but cannot name it: give it an accessible name with aria-label, as in aria-label="Films".

Buttons call riffle.next() and riffle.prev() (or riffle.goTo(index)), riffle.on('change') reports the active card and returns an unsubscribe, and riffle.destroy() undoes everything the engine did to your markup. From the full movie-stack example:

main.ts
function onPrev(): void {
riffle.prev()
}
function onNext(): void {
riffle.next()
}
prevButton.addEventListener('click', onPrev)
nextButton.addEventListener('click', onNext)
const unsubscribe = riffle.on('change', ({ index }) => updateMeta(index))
updateMeta(riffle.getSnapshot().activeIndex)
return function unmount(): void {
mql.removeEventListener('change', onMobileChange)
prevButton.removeEventListener('click', onPrev)
nextButton.removeEventListener('click', onNext)
unsubscribe()
riffle.destroy()
el.innerHTML = ''
}