Skip to content

Getting Started

Terminal
npm install @rpxl/riffle

A complete component. Pass it any array of objects with a title:

Quickstart.tsx
import { Riffle } from '@rpxl/riffle/react'
export function Stack({ films }: { films: { title: string }[] }) {
return (
// justifyContent keeps the stack's one grid column, and so every card, the
// card's own width (centred), so the fan scales about the card itself.
<Riffle
aria-label="Films"
cards={films}
cardWidth={300}
cardHeight={400}
style={{ justifyContent: 'center' }}
>
{(film, index) => (
<div
style={{
width: 300,
height: 400,
borderRadius: 16,
background: `hsl(${index * 72} 65% 45%)`,
color: 'white',
display: 'grid',
placeItems: 'center',
}}
>
{film.title}
</div>
)}
</Riffle>
)
}

That exact file, running. Drag the front card:

Neon Harbor
The Quiet Orbit
Salt and Static
Paper Moons
Last Train to Vela
Glasshouse
Northbound
Kite Season
  • <Riffle> renders one card per item in cards, from your render function, and stacks them in a single grid cell for the engine to move.
  • Every Riffle option is a prop: cardWidth and cardHeight here. Any other prop, such as aria-label or style, goes to the root element.
  • 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".

For your own markup instead of <Riffle>, useRiffle returns prop getters for the root and each card, plus next, prev and goTo. useRiffleState subscribes to one slice of the state, so this component re-renders when the active card changes, never on a drag frame:

HeadlessStack.tsx
const riffle = useRiffle({ count: films.length, cardWidth: WIDTH, cardHeight: HEIGHT, getLabel })
// Re-renders when the active card changes, never on a drag frame.
const activeIndex = useRiffleState(riffle, (state) => state.activeIndex)
<section {...riffle.getRootProps({ 'aria-label': 'Films', style: { width: WIDTH } })}>
{films.map((film, index) => (
<article
key={film.title}
{...riffle.getCardProps(index, {
style: {
width: WIDTH,
height: HEIGHT,
borderRadius: 16,
background: posterGradient(film),
},
})}
/>
))}
</section>
  • Adapter details: how options update between renders, refs, the imperative handle, and server rendering.