Clamp with controls
bounds: 'loop' is the default: drag past the last card and the first one comes back around. Set
bounds: 'clamp' instead for a stack that stops dead at both ends, the right choice for a fixed,
ordered sequence like a trail of waypoints or a numbered set of steps, where “after the last one”
should mean nothing rather than wrapping back to the start.
The data
Section titled “The data”Invented trail waypoints, each with a two-stop gradient standing in for a photo.
/** * Invented trail waypoints for the clamp demo. Each gets a two-stop CSS * gradient instead of a photo, since there is no licensed imagery to show. */export interface Waypoint { name: string elevation: string from: string to: string}
export const waypoints: readonly Waypoint[] = [ { name: 'Cedar Gate', elevation: '410 m', from: '#1a2e1c', to: '#5fae5c' }, { name: 'Long Ridge', elevation: '890 m', from: '#20242f', to: '#7b8fb8' }, { name: 'Quarry Bend', elevation: '650 m', from: '#332415', to: '#c98a4a' }, { name: 'Foxglove Meadow', elevation: '520 m', from: '#2a1730', to: '#b569c9' }, { name: 'Windrow Pass', elevation: '1120 m', from: '#122530', to: '#59b3c9' },]
/** A waypoint's background: a single diagonal two-stop gradient. */export function waypointGradient(waypoint: Waypoint): string { return `linear-gradient(155deg, ${waypoint.from}, ${waypoint.to})`}Genuinely disabled controls
Section titled “Genuinely disabled controls”The snapshot carries canPrev and canNext, false exactly when bounds: 'clamp' has nothing
further in that direction. Bind those straight to the native disabled attribute, not to a CSS class
that only looks disabled: a real disabled button drops out of the tab order’s activation entirely,
so a keyboard or screen reader user hits a control that does nothing to click, never a click that
silently fails.
<div class="controls"> <button type="button" class="control" aria-label="Previous waypoint" :disabled="!canPrev" @click="prev" > <span aria-hidden="true">‹</span> </button> <span class="readout">Card {{ activeIndex + 1 }} of {{ waypoints.length }}</span> <button type="button" class="control" aria-label="Next waypoint" :disabled="!canNext" @click="next" > <span aria-hidden="true">›</span> </button></div>
const { vRiffleRoot, vRiffleCard, activeIndex, state, next, prev } = useRiffle(() => ({ count: waypoints.length, bounds: 'clamp', cardWidth: CARD_WIDTH.value, cardHeight: CARD_HEIGHT.value, getLabel,}))
// Buttons are truly `disabled` at the ends, not just styled: a click, a tap// or an Enter key on a disabled button does nothing, and it drops out of// the tab order's activation, which a CSS-only affordance never gives you.const canPrev = computed(() => state.value.canPrev)const canNext = computed(() => state.value.canNext)Next: Programmatic control covers driving the stack from further outside than a pair of adjacent buttons.