Programmatic control
A prev/next button pair sits right next to the stack it drives. Plenty of real UIs need control from further away: a thumbnail rail, a “now playing” panel, a router that jumps to a specific card from a URL, a test harness. None of that needs to live inside, or even next to, the stack itself.
Every Riffle instance exposes goTo(index, opts?) alongside next and prev. Hand that instance
to whatever else needs it, and drive the stack from there.
The thumbnail rail below is a sibling, not a child, of the stack it drives. It never touches the
engine directly, only the film list it renders its own numbered buttons from, and moves the stack
purely by calling goTo(index).
const railButtons: HTMLButtonElement[] = []films.forEach((film, index) => { const button = document.createElement('button') button.type = 'button' button.className = 'rail-button' button.setAttribute('aria-label', `Jump to ${film.title}`) button.setAttribute('data-rail-index', String(index)) button.textContent = String(index + 1) button.addEventListener('click', () => riffle.goTo(index)) rail.appendChild(button) railButtons.push(button)})A few things worth calling out:
- The rail is a genuinely separate piece. It could just as easily be a completely different file, imported by neither the stack’s own component nor its parent, wired together only where both happen to be used together.
aria-current="true"marks the active thumbnail, read from the stack’s ownchangeevent, not from polling the handle’sactiveIndexgetter: that getter is there for imperative reads (an event handler, an effect), not for driving a render.goTotakes{ animate: false }when you want a jump to be instant instead of springing there, useful for a “restore this position” case (returning to a page, for example) where an animated approach would be visual noise rather than feedback.
Next: Forms inside cards covers the other direction: controls that live inside a card, not outside the stack.