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
manualmode a reveal control renders alongside the card. You can replace it with your own design system's button throughrenderRevealAction, but you cannot remove it. - In
automode 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-hiddenuntil 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.resolvingcovers 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,footerandtermsrender where you put them, with the content you supply. - Meaningful label text.
aria-labelon the card is yours to write. "Scratch card" is not a description; "October scratch card, 2 attempts left" is. - Timeouts. A
getOutcomethat 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.