# Accessibility policy

> Accessibility as a runtime contract: APG conformance, what every contract specifies, focus and portal policy, the test layers and the test matrix with its gaps.

Page: https://svelte-lean.vercel.app/docs/architecture/accessibility · Markdown: https://svelte-lean.vercel.app/docs/architecture/accessibility.md · Index: https://svelte-lean.vercel.app/llms.txt

Every behavior defines its semantics, keyboard, focus and ARIA in a written contract, is tested in a real browser, and is never traded for bytes.

## Policy

- **Native semantics first.** No added role where a native element has the widget model; otherwise the WAI-ARIA Authoring Practices pattern.
- **Correctness before bytes.** Order: correctness, accessibility, developer experience, performance, bundle size.
- **Behavior, not attributes.** Tests press ArrowRight and check focus moved.
- **Authored ids.** Server markup is correct before hydration and identical after.
- **axe and keyboard tests are not screen-reader testing**; the site says which it has.

## The contract

Every primitive ships a `contract.md` with the same sections, read here from the tabs contract at build time:

1. Required semantics
2. Required parts
3. Options
4. Application-rendered panel
5. Native part
6. Looks
7. Keyboard interaction
8. Focus rules
9. Disabled behavior
10. Orientation
11. RTL
12. ARIA relationships
13. Events
14. Platform features and Baseline
15. Attribute helper
16. SSR expectations
17. JS-disabled behavior
18. Runtime budget
19. Test cases

50 contracts, linked from each primitive page. Native ones also name their platform features, Baseline status and fallback ([Browser support](https://svelte-lean.vercel.app/docs/browser-support.md)).

## Focus and portal policy

No universal focus manager; each primitive has its own focus contract:

- Dialog: native `showModal()`; the rest is inert, closing returns focus to the invoker.
- Tabs: roving `tabindex`.
- Menu: first enabled item on open, the invoker on close.
- Combobox: focus stays in the input; `aria-activedescendant` marks the option.

No portal, overlay manager or z-index context: the top layer does it. Positioning: implicit anchor, then CSS anchor positioning, then the CSS fallback; no JavaScript positioning engine.

## Regression budget

A behavior change runs the contract's test cases in a real browser in the same gate as the size budgets. A byte saved never outweighs a failing keyboard test.

## Test layers

| Layer | What it proves | Where |
| --- | --- | --- |
| Contract tests | Tier, routed events, registration module and native base of every primitive | packages/primitives/tests/registry.test.ts |
| Algorithm tests | Next and previous enabled item, Home and End, RTL, typeahead matching | packages/core/tests, packages/primitives/tests/tabs.test.ts, menu.test.ts |
| Browser interaction tests | Focus, dialog, popover, top layer and anchor positioning in a real browser | apps/playground/tests/primitives/native.spec.ts, tabs.spec.ts, menu.spec.ts |
| Keyboard contract tests | Arrow keys, Home and End, activation modes, disabled skipping, Escape and focus return | apps/playground/tests/primitives/tabs.spec.ts, menu.spec.ts, table/table.spec.ts |
| JavaScript-disabled tests | The native contracts with page JavaScript off | apps/playground/tests/primitives/native.spec.ts |
| SSR tests | Server HTML with authored state and ids; hydration changes nothing | apps/playground/tests/ssr.spec.ts, packages/\*/tests/ssr.test.ts |
| Automated accessibility scans | No serious or critical axe violation on every page and on open dialog, popover and menu states | apps/playground/tests/primitives/a11y.spec.ts, apps/docs/tests/routes.spec.ts |
| Production bundle tests | The shipping invariants against real production output | fixtures/\*, apps/playground/tests/bundle.spec.ts |

Focus, top layer and anchor positioning run in Playwright; happy-dom unit tests cover the algorithms only.

## Test matrix

From the claim registry and the audit of 2026-09-27. "Not run": no recorded run (Firefox and WebKit run nightly, not on pull requests). "Not done": no session recorded.

| Area | Chromium | Firefox | WebKit | Screen reader |
| --- | --- | --- | --- | --- |
| Keyboard | tested in Chromium | not run | not run | not done |
| Focus | tested in Chromium | not run | not run | not done |
| axe scan | tested in Chromium | not run | not run | n/a |
| RTL | tested in Chromium (tabs) | not run | not run | not done |
| Without JavaScript | tested in Chromium | not run | not run | n/a |
| Reduced motion | stylesheet rule; no browser test | not run | not run | n/a |
| Forced colors | stylesheet rule; no browser test | not run | not run | n/a |
| Screen reader smoke | not done | not done | not done | not done |

Recorded Firefox and WebKit runs are a stage 2 requirement, a screen-reader session per primitive (VoiceOver with Safari, NVDA with Chrome or Firefox) stage 3 (docs/release.md).

## What is not claimed

- Screen-reader behavior, in any browser.
- Behavior in Firefox or WebKit on every pull request: the playground suite runs there in the nightly cross-browser job (`SLEAN_BROWSERS=all`), not in the pull-request job.
- IME composition with an operating-system input method: the autocomplete and the select search are tested through the DevTools protocol only.

## Source

- apps/playground/tests/primitives: the browser suite
- a11y.spec.ts: the axe scans
- packages/primitives/src: one contract per primitive
- docs/proof/claims.md: the status of every claim
