Forms inside cards
A card is not required to be read-only content. If it holds a real form field, two pieces of engine behaviour, both already covered in Accessibility, are what make that work without any extra configuration on your part.
Typing does not move the stack
Section titled “Typing does not move the stack”Riffle checks the keydown’s own target before mapping arrow keys, Home or End to a stack move
(see Accessibility). An <input>,
<textarea>, <select>, or a contenteditable element is left alone entirely: the keydown reaches
the field itself, so the caret moves and characters get typed exactly as they would outside a stack.
There is nothing to opt into.
A background card’s field is unreachable, not just visually behind
Section titled “A background card’s field is unreachable, not just visually behind”Every card except the active one is inert (see Accessibility).
An <input> inside a background card cannot receive focus: not by Tab, not by a screen reader’s
virtual cursor, not by a direct .focus() call from your own code. Advance the stack in the live demo
above, then Tab through the page: only the front card’s field is ever reachable, and the note you
typed on a card you have since navigated away from stays exactly where you left it, simply out of
reach until that card is active again.
Where focus goes when the active card changes
Section titled “Where focus goes when the active card changes”If a field inside the active card is focused and the stack moves to a new card, focus follows, exactly as Accessibility describes for any card’s content: it lands on the new active card’s own root element, never on a field inside it, and never the specific field you happened to be in before.
If you want focus to land inside the new card’s first field instead, put it there yourself, in your own change handler: the engine updates the new card’s accessibility state, including its own focus-follow onto that card’s root, before it fires its own change event, so your own redirect runs after the engine’s and is what sticks.
onChange={(event) => { setActiveIndex(event.index) // Only redirect focus into the new field when focus was already // somewhere inside the stack: that is exactly the condition the // engine's own focus-following uses to decide whether to move // focus onto the new active card's root at all. Skipping this // check would steal focus into a card field even when nothing on // the page had focus in the stack, for example while a visitor // was reading unrelated page text and something else called // goTo(). const wasFocusInStack = document.activeElement?.closest('[aria-roledescription="carousel"]') != null if (!wasFocusInStack) return // The engine updates the new active card's accessibility state, // including its own focus-follow onto that card's root, before it // fires this handler, so this call is the last one to move focus, // not the first, and it is the one that sticks. noteFieldRefs.current[event.index]?.focus()}}Each card’s own field
Section titled “Each card’s own field”const inputId = `${idBase}-note-${index}`return ( <div style={{ width: CARD_WIDTH, height: CARD_HEIGHT, borderRadius: 16, boxSizing: 'border-box', padding: 16, display: 'flex', flexDirection: 'column', gap: 8, background: '#1c1c26', color: '#f3efe8', border: '1px solid rgb(255 255 255 / 8%)', boxShadow: '0 24px 48px -16px rgb(0 0 0 / 60%), 0 2px 6px rgb(0 0 0 / 35%)', }} > <strong>{film.title}</strong> <label htmlFor={inputId} style={{ fontSize: '0.8rem' }}> Your note </label> <input id={inputId} ref={(el) => { noteFieldRefs.current[index] = el }} type="text" placeholder={`What did you think of ${film.title}?`} style={{ padding: 8, borderRadius: 8, border: '1px solid rgb(255 255 255 / 24%)' }} /> </div>)A few things worth calling out:
- Each input’s
idis built fromuseId()plus the card’s index, so<label htmlFor>genuinely targets the right field even though every card renders the same JSX. A hardcodedidwould collide across cards, and across multiple stacks on one page.
- The label is real markup, not a placeholder standing in for one. A
placeholderdisappears the moment you type and most screen readers do not treat it as a substitute for a label; every version of this recipe uses both, a real<label>for the accessible name and aplaceholderfor the example prompt text. - Nothing here is Riffle-specific. The stack does not know or care that its cards contain form
fields; every behaviour on this page falls out of the roving-tabindex and
inertmechanics that apply to any element inside a card, form field or not.
You have reached the end of the recipes. Examples is the full gallery if you want to see every shipped example in one place.