Accessibility
SquareKit is built to be accessible by default, but using SquareKit does not by itself make a product accessible: what SquareKit guarantees, what it inherits from the layers underneath it, and what you still own every time you build a screen.
Our conformance target
SquareKit targets WCAG 2.2 Level AA. Meeting that target in a shipped product depends on how components are composed, labeled, and filled with content. SquareKit gives you an accessible floor; it cannot give you an accessible product. What has and hasn’t been verified lives in Known gaps on this page.
Use POUR as a framing check
The four WCAG principles help teams ask complete questions. They do not prove that a screen or component has passed an accessibility audit.
Perceivable
Operable
Understandable
Robust
How accessibility works in SquareKit
SquareKit is four layers. Each is responsible for different things, and knowing which layer owns what is the fastest way to debug an accessibility problem. The short version: behavior is inherited, appearance is ours. Almost every accessibility defect we are likely to ship is an appearance or composition defect, not a behavior defect.
Layer | What it is | Responsible for |
|---|---|---|
| React Aria | Headless behavior and semantics | ARIA, keyboard, focus, announcements, i18n |
| Tailwind CSS | Utility styling | Nothing semantic; expresses our visual decisions |
| Upstream React kit | Styled components on React Aria | Structure and default styling |
| SquareKit | Theme, tokens, composition | Contrast, focus, targets, motion, labeling |
What React Aria gives us
React Aria is Adobe’s headless component library, maintained as part of React Spectrum. It implements semantics and keyboard behavior per the W3C ARIA Authoring Practices Guide and WAI-ARIA. Everything below comes free on React Aria–backed components.
Keyboard interaction
Focus management
Keyboard-only focus rings
No sticky hover on touch
Press semantics
Touch screen readers
Internationalisation
React Aria tests against VoiceOver on macOS (Safari and Chrome), JAWS on Windows (Firefox and Chrome), NVDA on Windows (Firefox and Chrome), VoiceOver on iOS, and TalkBack on Android (Chrome).
What React Aria explicitly does not do. You are responsible for an accessible visual design: meaningful labels, sufficient color contrast, adequate hit target sizes, visible focus rings, and respecting motion preferences. Every one of those is SquareKit’s job, not React Aria’s.
Check the false-positives list before filing a bug. React Aria maintains a list of known false positives that automated accessibility tools report against its components. Cross-check an axe or Lighthouse run against it before filing a SquareKit bug.
Before filing a SquareKit bug from an axe or Lighthouse run, check React Aria’s known false positives wiki.
What Tailwind CSS does and does not do
Tailwind is presentational. It has no semantics, no roles, and no keyboard behavior. It cannot make a component accessible, and it cannot make one inaccessible, except through the visual decisions we express with it, which is precisely where most accessibility failures live.
Utility / variant | Use | In the kit today |
|---|---|---|
sr-only | Visually hidden text for screen readers | 26 uses |
aria-hidden | Hiding decorative elements from AT | 167 uses |
focus-visible: | Keyboard-only focus styling | 331 uses |
outline-hidden | Forced-colors-safe outline removal | 83 uses |
motion-reduce: / motion-safe: | Respecting prefers-reduced-motion | 21 motion-reduce: uses |
contrast-more: / forced-colors: | High-contrast and forced-colors modes | forced-colors: adopted 2026-08-22 across the base fields and selects, radios, button groups, badges, progress bars, the avatar status dot and featured icons, and on the application section’s breadcrumbs, color pickers, command menus, pagination and tabs (activity feeds and notifications got real borders instead); contrast-more: not yet |
pointer-coarse: / pointer-fine: | Larger targets on touch | available, not yet adopted |
The outline-none trap
In Tailwind v3, outline-none did not actually remove the outline; it applied a 2px transparent outline, which stays visible in forced colors mode. Tailwind v4 renamed that behavior to outline-hidden and introduced a new outline-none that genuinely sets outline-style: none. This matters because box shadows are not rendered in forced colors mode: a focus ring built from shadow-* or ring-* on top of outline-none vanishes for a Windows High Contrast user.
Never use outline-none
Use outline-hidden and pair it with an explicit visible focus style that is not itself an outline-* utility. In Tailwind v3 outline-none applied a 2px transparent outline that stays visible in forced colors mode; v4 renamed that to outline-hidden and made outline-none genuinely set outline-style: none. Box shadows are not rendered in forced colors mode, so a focus ring built from shadow-* or ring-* on top of outline-none vanishes for a Windows High Contrast user. One pairing does not compose in v4: outline-hidden sets --tw-outline-style to none, and focus-visible:outline-2 reads that variable back as its outline-style, so the ring never paints. Measured 2026-08-22 on CloseButton, which had no keyboard ring that way until its focus:outline-hidden was dropped. When the ring is an outline-* utility, leave outline-hidden off, since browsers draw no default ring on pointer focus, or add focus-visible:outline-solid. The vendored kit follows this today: 83 outline-hidden uses, zero outline-none.
What SquareKit owns
SquareKit inherits behavior from React Aria and source structure from an upstream React kit. Once that code is vendored, its maintenance, visual accessibility, and composition are all SquareKit responsibilities.
Inherited behavior, owned outcomes
The upstream kit is built on React, Tailwind CSS v4, TypeScript, and React Aria. Its accessibility claim describes React Aria’s guarantees, not an independent audit or VPAT. Marketing sections, page examples, charts, and every SquareKit customization still require our review.
Accessibility by inheritance
Not everything is React Aria
Charts are not covered
We own the code
SquareKit maintainers track the upstream changelog and React Aria releases, pull accessibility fixes in per release, and record the versions SquareKit is aligned to on every sync.
Color and contrast
SquareKit re-themes the upstream kit rather than using its palette as shipped, which means none of the upstream palette’s contrast characteristics carry over to our tokens. Contrast is our responsibility to verify and re-verify whenever the palette changes.
Body text
Large text
UI boundaries
Both themes
Never color alone
Focus visibility
--color-focus-ring (brand-500) rendered as a 2px outline with a 2px offset via focus-visible:outline-2 focus-visible:outline-offset-2, with --color-focus-ring-error (rose-500) for invalid fields. Every interactive element shows it on keyboard focus, it must meet 3:1 contrast against the adjacent background, and focus order follows visual reading order — with the one narrow exception below. Both rings are rendered live on the Effect styles page. See the outline-none rule above for the implementation constraint.
Rings drawn statically: brand-500 and rose-500, 2px at a 2px offset; tab anywhere on this site for the real thing
Target size
- Interactive targets are at least 24×24 CSS pixels. WCAG 2.2 AA, success criterion 2.5.8.
- Grow the hit area, not the visual. Where a control is visually smaller, expand the hit area with padding rather than by scaling the visual.
- Space adjacent targets. Adjacent targets have enough spacing that the 24px exception never has to apply.
✓ Pad the hit area
✗ A bare small target
Motion
- Every animation gets a reduced-motion path. The standard and its current honest status live on the Motion page.
- Nothing flashes more than three times per second.
- Auto-playing motion longer than five seconds can be paused. Paused, stopped, or hidden.
- Parallax and large-scale movement always respect the preference. They are vestibular triggers, so they always respect prefers-reduced-motion.
Reduced motion removes the movement and keeps the fade: the state change stays legible
Labeling and content
React Aria associates a rendered <Label> with its control automatically. Our responsibility is that the label exists and says something useful; the full standard lives on the Content design page.
A visible label, always
aria-label="Delete invoice"Icon-only still has a name
Names carry the object
Errors that help
aria-labelaria-hiddenMeaningful vs decorative icons
Headings are structural
Composition
Individually accessible components can compose into an inaccessible page. The rules that keep a page whole:
One h1 per page
Landmarks present and unique
A skip link, reachable first
Focus follows the action
Dialogs return focus
Live regions, sparingly
How we test
The process as it actually runs today: systematised where the tooling exists, manual where it doesn’t yet.
- The accessibility review gate. UI changes run through the plugin’s /accessibility-review skill and its read-only review agent before merge: semantic HTML, labels, keyboard navigation, focus, ARIA usage, and contrast, producing a pass/fail report with required fixes.
- The template gates. Every change passes lint, typecheck, unit tests, a production build, and the Playwright e2e suite.
- Keyboard pass. Tab through the changed flow: every interactive element reachable, in a sensible order, with a visible focus ring, and no keyboard traps.
- Both themes. New and changed surfaces are checked in light and dark mode.
- Not yet systematic. Automated axe scans, screen reader passes, per-token contrast verification, forced-colors and reduced-motion emulation. These are listed honestly in Known gaps.
Known gaps
A short, honest gap list is worth more than a long list of principles. What has not been done, as of the last update of this page:
- No component has been systematically screen-reader tested by us; we inherit React Aria’s testing, and spot-checks are ad hoc.
- The SquareKit palette has not been contrast-verified pair-by-pair across both themes; verification is per-change judgement.
- The kit carries motion-reduce: variants only on its CSS loops (the loading-indicator spinners and the empty-state and social-proof marquees); its motion-library usages do not call useReducedMotion(), so the reduced-motion standard holds only there, for new work, and for the docs site.
- Target sizes have the same status as contrast: required, not yet audited pair-by-pair across the kit’s smallest controls.
- Presentational (non–React Aria) kit components, such as marketing sections and page examples, have not been individually reviewed.
- Chart components have no built-in text alternatives; each use needs its own.
- This documentation site has not itself been audited against the target.
Checklist
The per-screen distillation of everything above. Before UI ships:
- POUR covered: the screen is perceivable, operable, understandable, and robust
- Every interactive element is keyboard-reachable, in a sensible order, with the visible focus ring, and no keyboard traps
- Text contrast 4.5:1; large text and UI boundaries 3:1, in both themes
- Interactive targets at least 24×24px, hit areas padded rather than visuals scaled
- Every input has a visible label; every icon-only button an aria-label
- Color is never the only signal: status and validation carry text, icon, or shape
- Headings are structural, landmarks present, focus moves with newly revealed content
- Every animation has a reduced-motion path
- At 400% zoom, content reflows without clipping or loss of function
- Automated scan results cross-checked against React Aria’s false-positives list
- Anything skipped is recorded in Known gaps, not left unstated
Reporting an accessibility issue
Open a GitHub issue on the repository. Include the component, the assistive technology and browser used, what you expected, and what happened. Accessibility issues are prioritized above other bug classes.
References
The primary sources behind every claim on this page.
