Focus & a11y
The two-layer focus contract
A focus ring is only useful if the thing can be focused. In this kit those are two separate facts, and they are enforced together:
focusRingStyledraws the ring on the web, keyed to:focus-visible;- the frame must also be focusable — a real
<button>/<input>, or aBoxmade focusable throughuseKeyboardActions.
A guardrail test asserts the pairing: a component that applies the ring without being focusable fails CI. That exists because the failure is invisible in review — the styles look right, the ring simply never fires.
What the kit gives you
- Real semantic elements on the web.
Buttonrenders<button>,TextInputrenders<input>,Anchorrenders<a href>. Keyboard activation, form participation and screen-reader roles come from the platform, not from ARIA patches. - Keyboard maps on composite widgets.
Menu,Combobox,Select,Tabs,Accordion,Pagination,StepperandTreeimplement arrow-key navigation,Home/End, type-ahead where it applies, andEscapeto dismiss. - Focus trapping in overlays.
Modal,DrawerandDialogtrap focus while open and restore it to the trigger on close.FocusTrapis available directly. - Labelled fields. Every input wires
label,descriptionanderrorto the control with generated ids, so the accessible name and description are correct without you passingaria-*. - Live regions.
Alertsetsrole="alert";Notificationannounces.
What you still owe
-
An accessible name for icon-only controls. An
ActionIconwith no label is a mystery to a screen reader:<ActionIcon aria-label="Delete item"><IconTrash /></ActionIcon> -
Meaningful
alton images.Imageforwards it; emptyalt=""for decoration is a choice you have to make. -
Colour is never the only signal. The variant ladder gives you fill, border and text together — use text or an icon as well as
theme="red". -
Order. Focus order follows DOM order on the web and mount order on native; visual reordering with absolute positioning does not reorder focus.
Native differences
Notable native-only behaviours the kit handles for you:
readOnlyinputs on Android receive no press events, so focus-driven overlays needwithPressToggle={false};- dropdown lists need
keyboardShouldPersistTaps="handled", or the first tap is swallowed by the keyboard dismissing; - the keyboard is handled with
KeyboardAvoidingViewand the RNKeyboardAPI, not a third-party controller.
Testing it
Component tests in this repo assert real host elements — getByRole("button")
rather than a testID — precisely so a refactor that drops semantics fails.
Copy that habit: if your test can pass with a <div>, it is not testing
accessibility.