Loyalty Patternsv1.00

Accessibility

Accessibility in this kit is structural. It is not a prop, not a mode, and not something you enable — the components are built so that the accessible path is the only path, because gesture-driven reward mechanics are one of the easier places to lock people out without noticing.

Every gesture has an equivalent

Scratching, spinning and dragging are path-based gestures. WCAG 2.5.1 (Level A) requires a single-pointer equivalent for each one, and the kit provides it rather than leaving it to you.

What that looks like in the scratch card:

  • In manual mode a reveal control renders alongside the card. You can replace it with your own design system's button through renderRevealAction, but you cannot remove it.
  • In auto mode there is no second control, because the panel itself is a real <button> — one tap, or Enter, does everything.

This is worth stating plainly because it is the requirement people argue with: the button is not there for keyboard users as a special case. It is there for anyone with a tremor, limited dexterity, a trackpad they dislike, or a cracked screen. Those users vastly outnumber the ones a "keyboard fallback" framing implies.

Results are announced, not just shown

A revealed prize is a state change that a sighted user perceives instantly and a screen-reader user perceives only if you tell them.

  • Prize content is aria-hidden until it is actually revealed, so nobody hears the outcome before earning it.
  • The result is announced through a polite live region using your own copy, via labels.announce(outcome).
  • Focus moves to the prize on reveal, so the next Tab lands somewhere sensible.
  • In-flight states announce too — labels.resolving covers the window where the server has been asked but has not answered.

Phrase resolving as checking, never calculating. The server already decided; implying live computation is both inaccurate and, for a chance mechanic, the wrong impression to leave.

Motion is the OS's decision

tokens.css sets --lk-motion: 0 under prefers-reduced-motion, which collapses every animation in the kit to an instant state change. No component reads the media query itself, and no component offers a motion prop that could override it.

The kit deliberately has no motion toggle in its own UI. Supporting one would mean writing --lk-motion inline, which beats the media query and takes the choice away from someone who already made it at the OS level.

Copy is yours, including the invisible copy

Every string arrives through labels, and the type enumerates all of them — including screen-reader announcements. There is no English fallback baked into a component, which means there is no way to ship a half-translated experience without the type system telling you.

Direction

The scratch card's painted hint follows the document's text direction, so RTL campaigns render correctly rather than mirroring the layout and leaving the canvas text backwards. Canvas is easy to get wrong here, because it does not inherit direction the way DOM text does.

What the kit cannot do for you

Being honest about the boundary is part of the contract:

  • Contrast of your chosen colours. The playground corrects the action colour against the surface, but a component cannot inspect campaign artwork to know whether your hint text is legible on it. Pass foil.textColor, or hide the hint.
  • Reading order of your own slots. header, footer and terms render where you put them, with the content you supply.
  • Meaningful label text. aria-label on the card is yours to write. "Scratch card" is not a description; "October scratch card, 2 attempts left" is.
  • Timeouts. A getOutcome that never settles leaves the card announcing checking forever. Enforce a timeout in your own fetch.

Testing it

The fastest useful check is not a scanner. Load a component, put the mouse away, and try to win with the keyboard alone. Then do it again with VoiceOver or NVDA on and your eyes closed. Anything that fails that is a real defect; a scanner will not find most of them, and it will flag several things that are fine.