Accessibility Conformance Report
A VPAT-2.5-shaped report — WCAG 2.2, Level A + AA (applicable subset for a CSS/JS UI toolkit — criteria with no toolkit surface, e.g. captions/audio, are omitted, not silently marked Supports) — generated from the same facts the build gates already produce (contrast pairs, keyboard map, event/ARIA coverage, forced-colors emulation), not typed by hand. This is not a substitute for a licensed accessibility auditor's ACR — it's the honest, machine-checkable slice: automated evidence is cited by number and regenerates every build, and a row with no automated evidence is marked Not Evaluated, never inferred as Supports.
Summary
10 Supports 6 Conditional-on-adopter 3 Not Evaluated 2 Partially Supports 21 criteria
Automated evidence (contrast, keyboard-map, event/ARIA generation, forced-colors emulation) is cited by number and regenerates every build; rows with no automated evidence are Not Evaluated, never inferred as Supports.
Criteria generated
| SC | Criterion | Level | Conformance | Remarks |
|---|---|---|---|---|
| 1.1.1 | Non-text Content | A | Conditional-on-adopter | Icons (icon) are decorative masks — the adopter's visible label carries the meaning, per the documented aria-hidden convention. An icon-only control must supply its own aria-label; the framework cannot author that text. |
| 1.3.1 | Info and Relationships | A | Conditional-on-adopter | Structure rides on real semantics — native <table>/<dialog>/<details>, ARIA roles/states in markup contracts across 40 components. Relationships the adopter supplies (label text, row data) are outside framework control. |
| 1.4.1 | Use of Color | A | Supports | Two-channel doctrine: every state signal ships a visible non-color cue (glyph, border, position) alongside color. Be precise about what backs that. The forced-colors list below (18 of 40 components) is parsed from the CSS, and check-forced-colors.mjs verifies each shipped rule's selector still matches real markup and that emulation changes the declared properties — so the rules are proven LIVE rather than merely present. What no gate checks is the doctrine itself: that every state signal carries a non-color channel. That remains a review rule applied per component, not an automated proof, and this row should not be read as one. |
| 1.4.3 | Contrast (Minimum) | AA | Supports | 80/80 token pairs pass across both base themes, and a further 480/480 across 6 brand presets x 2 themes — computed from shipped token values on every build (check-contrast.mjs); a failing pair blocks the release. Read the number precisely: of the 40 pairs per theme, 29 are text at the 4.5:1 AA floor and 11 are non-text at 3:1 (those are 1.4.11's, not this row's), so this is not 80 readings all tested at 4.5. |
| 1.4.4 | Resize Text | AA | Conditional-on-adopter | Sizing uses rem/relative units throughout; the installation skeleton explicitly forbids user-scalable=no. An adopter who hardcodes px or adds that meta tag overrides this. |
| 1.4.10 | Reflow | AA | Supports | Every docs page is swept in CI at 390px AND at 1432px under 150% browser zoom, measuring the shell scroller rather than the document (a 100dvh shell never grows, so document scrollWidth silently reports nothing). Wide content scrolls inside its own container instead of the page. |
| 1.4.11 | Non-text Contrast | AA | Partially Supports | 11 of the 40 gated token pairs per theme are non-text and run in the same contrast.json gate as the text pairs, at 3:1 — so there is no separate, weaker check. Edges are adjudicated on every build (check-contrast.mjs, roadmap 374.7): of the 28 edge pairings the component CSS paints, 6 are gated at 3:1, 17 are exempt with a stated reason naming the other channel that identifies the element, and 5 are tracked as debt because the edge is the only non-text identifier and does not meet 3:1: button (border-strong on bg-surface): 14 of 14 theme and brand readings under 3:1, spanning 1.47-1.70:1; file-upload (border-strong on bg-surface): 14 of 14 theme and brand readings under 3:1, spanning 1.47-1.70:1; file-upload (border-strong on bg-muted): 14 of 14 theme and brand readings under 3:1, spanning 1.34-1.45:1; filters (border-strong on bg-muted): 14 of 14 theme and brand readings under 3:1, spanning 1.34-1.45:1; file-upload (accent-solid on bg-selected): 7 of 14 theme and brand readings under 3:1, spanning 1.82-9.90:1. The focus ring is gated on bg-canvas alone; its other grounds are 2.4.7's row. Tracked as roadmap 374.4. This verdict is computed from that debt list, and it reads Supports when the list is empty. |
| 1.4.12 | Text Spacing | AA | Supports | The user spacing override is applied to every docs page at two widths in CI, comparing clipping BEFORE and AFTER, so a deliberate ellipsis is not misreported and real content loss is. Heights are minimums, never fixed, for this reason. |
| 1.4.13 | Content on Hover or Focus | AA | Not Evaluated | Popover-based surfaces (combobox, dropdown, dialog) dismiss via native popover/Esc, which is dismissible/hoverable by construction — but "persistent until dismissed or invalid" per-surface has not had a dedicated manual pass. |
| 2.1.1 | Keyboard | A | Supports | 4 behaviors ship documented, generated keyboard support (initCombobox, initDataGrid, initDialogs, initTabs); everything else is native (a plain input, a native dialog's own focus trap) or has no keyboard surface — see the generated table on JS behaviors. |
| 2.1.2 | No Keyboard Trap | A | Supports | dialog uses a real focus trap (Tab/Shift+Tab cycle, Escape closes via native showModal()) — never an unclosable trap. data-table's grid-nav Escape returns focus from a cell's widget to the cell. |
| 2.4.3 | Focus Order | A | Not Evaluated | DOM order matches visual order everywhere audited manually this session, but a systematic focus-order sweep across all components has not been performed. data-table's row-edit Save/Cancel move focus to the row's first field before hiding themselves (an adversarial-review find: focus previously dropped to <body> mid-table). |
| 2.4.7 | Focus Visible | AA | Partially Supports | A single :focus-visible ring token (--bo-color-focus-ring) is used framework-wide and is never suppressed: 0 `outline: none` in packages/core/src/css, counted on every build (not gated). Its contrast FAILS the build only on bg-canvas; the universal ring rule uses outline-offset, so the ring paints on whatever sits behind the control. Measured on every build against 5 grounds (bg-canvas, bg-surface, bg-surface-raised, bg-muted, bg-selected) in both themes and every brand preset, 70 readings: the lowest is 2.99:1 (brand-forest-light, on bg-muted), and 1 reading(s) fall under the 3:1 floor: 2.99:1 on bg-muted (brand-forest-light). Same root cause as 1.4.11 (roadmap 374.4). This verdict is computed from those readings. |
| 2.4.11 | Focus Not Obscured (Minimum) | AA | Supports | The sticky action bar is the one surface that could cover a focused control; CI focuses every field on the detail-form pattern and asserts none intersects the bar. Executed, not reasoned about. |
| 2.5.7 | Dragging Movements | AA | Conditional-on-adopter | Derived, not asserted: a scan of the 31 shipped source modules under packages/core/src/js shortlists 3 (behaviors/combobox.ts, behaviors/context-menu.ts, behaviors/file-dropzone.ts); after review the author-defined dragging surface is 1 — behaviors/file-dropzone.ts — and behaviors/combobox.ts, behaviors/context-menu.ts are non-drag matches (pointermove keeps the pointer-hovered option in sync with the keyboard-active one, and mousedown on an option is cancelled only so focus stays in the field (375.10) — nothing is moved, and there is no dragstart or pointer capture; pointerdown/pointerup only record whether a press is in progress, so a menu asked for mid-press opens after the release (377.1) — nothing follows the pointer and nothing is moved). Reproduce with: grep -rlE "['\"](drag(start|enter|over|leave|end)?|drop|pointer(down|move|up|cancel)|mouse(down|move|up)|touch(start|move|end)|draggable)['\"]|\b(setPointerCapture|releasePointerCapture|DataTransfer|dataTransfer)\b" packages/core/src/js. Each drag surface carries a single-pointer alternative that does not require dragging: behaviors/file-dropzone.ts — the whole visible `.bo-file-dropzone` is a <label> wrapping the file input, so a single click anywhere in the box opens the picker — native label forwarding, no JS, no path along a screen. The framework itself therefore offers no function that REQUIRES a dragging movement. Two limits are part of this claim, not footnotes to it: the scan reads literal event names, so a fully computed listener name, a drag arriving through a dependency, or drag written in an adopter's own screens is invisible to it; and keyboard operability is NOT evidence for this criterion — it is 2.1.1, and the alternative cited above is a pointer path. Adopters own any dragging they add themselves, and the non-drag equivalent for it. |
| 2.5.8 | Target Size (Minimum) | AA | Supports | Conformance here rides on the SPACING EXCEPTION, not on a 24px floor: checkboxes, radios, the tag-input remove button and the data-table sort button are 16-18px, and CI runs the actual spec test (a 24px circle centred on each undersized target must not reach another target) across seven control-dense pages in all three densities. Adopters who crowd controls tighter than 24px between centres break this — the framework cannot guarantee it for markup it did not author. |
| 3.2.1 | On Focus | A | Not Evaluated | Reads true on inspection — no shipped behavior appears to trigger navigation or a context change purely on focus, and the state changes (open, filter, activate) are click/keydown-gated. But it is prose: no gate asserts it, and this file's own methodology says rows with no automated evidence are Not Evaluated, never inferred as Supports. Applying that rule to this row rather than exempting it. The check it wants is mechanical — focus every interactive element on the built pages and assert no navigation, no popover open and no 10-strong intent event fires — so this returns to Supports when that exists, not on re-inspection. |
| 3.3.1 | Error Identification | A | Conditional-on-adopter | The form error contract (aria-invalid + aria-describedby + a message element, role="alert" if dynamic) is documented and demoed at both field and cell granularity (editable-grid pattern) — but the adopter writes the actual error text and wires aria-invalid from their own validation. |
| 3.3.2 | Labels or Instructions | A | Conditional-on-adopter | Every form field demo shows a real <label for>; the framework enforces nothing at build time that would catch an adopter shipping an unlabeled input (no axe-in-CI gate on consumer apps, only on these docs). |
| 4.1.2 | Name, Role, Value | A | Supports | 10 of the 26 shipped behaviors write ARIA state in response to interaction, and the attributes they manage are derived from source rather than listed by hand: aria-activedescendant, aria-colindex, aria-current, aria-expanded, aria-live, aria-multiselectable, aria-orientation, aria-rowcount, aria-rowindex, aria-selected. The other 16 set none, and several are correct that way — dialog uses native showModal(), dropdown the native popover, so the platform owns the state. Two limits are part of this claim: the scan reads literal setAttribute calls, so state written through a computed attribute name is invisible to it; and aria-sort is deliberately NOT in the list above — the data-table sort header is inert without app code, which check-claims.mjs asserts by pressing Enter and confirming the attribute does not move. 10 intent events and the generated event/keymap tables exist so this row has evidence instead of prose. |
| 4.1.3 | Status Messages | AA | Supports | Live-region behaviors ship for the cases that need one without a focus change: data-table selection count and RF-scanner confirmation (initScanInput, JS-only — no CSS surface of its own), skeleton loading regions (role="status"). |
Forced colors — components with explicit rules generated
18 of 40 components
carry an explicit forced-colors rule (most others survive on structure
alone — border, glyph, or text state that forced colors doesn't touch; see the
accessibility model for the two-channel
reasoning). Scanned from source, not asserted:
avatarbadgebuttoncalendarcomboboxdata-tabledialogformiconoffcanvasprogressscansegmentedskeletonsteppertabstag-inputtree-table
What "Not Evaluated" means here
Two criteria above, plus the broader assistive-technology ledger (VoiceOver, NVDA verbalization), require a human running real AT hardware — nothing this build environment can automate. They stay Not Evaluated rather than being inferred Supports from adjacent automated passes. Runtime evidence, once gathered, gets recorded and cited here — see the honest-status ledger.