Skip to content

Adapter details

The rest of the Vue adapter, after Getting Started: <Riffle> and its typed slot, the template ref, how option changes reach the engine, the directives outside <script setup>, and server rendering.

If you also import from the core directly, for example fan() to tune the layout, no extra install is needed: both entries ship in the same package.

The drop-in component: pass cards and a #card="{ card, index }" slot, and it wires up useRiffle for you. Every Riffle option is a prop, plus getKey for lists that reorder and cardClassName for a class on every card element. Attributes that are not props, such as id, class, style or aria-label, fall through to the root element as usual in Vue.

In a .vue template (or JSX), the slot’s card is typed by whatever array you bind to cards, the same way a <script setup generic="T"> component’s slot would be: :cards="films" types #card="{ card }"’s card as films’s element type, not unknown. This inference comes from Vue’s DefineSetupFnComponent generic constructor type, cast onto the component the adapter builds. That type first ships in Vue 3.4.20, which is why the adapter requires vue@^3.4.20.

That inference is a template-compiler feature, so it only reaches a template. A component that builds <Riffle> with h() instead does not get it: h()’s own typing does not carry a component’s generic through a call the way the template compiler does, so a slot passed to h() gets no inferred type for card at all (an unannotated one is an implicit any error under strict settings). Annotate the slot callback’s parameter yourself, typically unknown, then narrow card with a cast.

A template ref on <Riffle> receives a RiffleInstance: next, prev, goTo, on, instance (the live engine, or null while unmounted), activeIndex and state (the current snapshot). activeIndex and state are reactive when read in a template or a computed.

useRiffle(options) accepts a ref, a getter, or a plain options object, and returns state, activeIndex, the two directives, and the imperative handle (next, prev, goTo, on, instance).

Options are compared with the previous set, and only what changed reaches the engine’s update(). spring and rotation compare shallowly, so a fresh object with the same values is not a change. Every other option, layout and getLabel included, compares by identity, so define them once (module scope, or once in setup) rather than creating a new object or function each time the getter runs. Only two changes rebuild the engine instead, keeping its position: a new axis, and cardWidth or cardHeight switching between a number and 'auto'.

startIndex is read once, when the engine is first built: call goTo to move later.

Attach v-riffle-root to the stack container and v-riffle-card="index" to each card. In <script setup>, destructure vRiffleRoot and vRiffleCard from useRiffle at the top level, under exactly those names: <script setup> exposes a top-level binding named vSomething to its template as the directive v-something, and that is the only reason the template can see them. Renamed, or kept inside an object, they do not resolve.

Outside <script setup>, a template cannot use a setup binding as a directive, and a component’s directives option is shared by every instance, so it cannot hold one engine per instance. Use a render function instead, and pass the directives to withDirectives: either the pair useRiffle returns, or a pair you build with createRootDirective and createCardDirective from a handle you create yourself with createAdapterHandle from @rpxl/riffle, as examples/vue-movie-stack/src/RenderFunctionStack.ts does.

Both directives are element-aware: Vue unmounts a removed card synchronously during patch but runs updated hooks after it, so a per-index ref callback would have unregistered whichever card had just moved into that slot instead of the one that left.

During server rendering the directives render the root with display: grid and data-riffle-root, and each card with its grid cell and data-riffle-card, so the stack is already stacked before hydration. On the server that display: grid is unconditional, where on the client it only applies when you have not already set an inline display yourself.

Moving off the legacy vue-card-stack package? See Migration from vue-card-stack.