VPAT® 2.5 EU — EN 301 549
⚠️ DRAFT — not signed. 10 criteria require human attestation before this report is final.
| Field | Value |
|---|---|
| Product | fcharts |
| Version | 0.1.0 |
| Description | Charts that render 100k+ points at 60fps and pass the accessibility audit at the same time. |
| Report date | 2026-06-05 |
| Standards | EN 301 549 (the EAA harmonized standard); Chapter 9 references WCAG 2.1 Level A/AA. This report maps WCAG 2.2 Level A + AA — a superset of 2.1 — so the EN 301 549 web requirements are fully covered, with the 6 newer 2.2 criteria as additional assurance. |
| URL | — |
| Evaluation methods | Automated axe-core scan (scoped to the chart); Functional probes: keyboard navigation, zoom, Escape-dismiss, live-region announcement; Computed contrast on library-controlled pairs; DOM-semantics assertions (roles, names, table, target size); Manual attestation for perceptual + real-AT + integration-context criteria |
| Component scope | Everything the library renders inside its container (.fc-root): the <canvas> data layer, the DOM axis ticks, the legend, the focusable data surface, the live region, the hidden data table, the readout, and the embedded JSON summary. Page-level criteria are the host application’s. |
Automated/hybrid rows are re-proven on every run by this gate; manual-attestation rows are listed for human sign-off. axe-clean alone is necessary, not sufficient.
33 Supports · 2 Partially Supports · 20 Not Applicable across 55 WCAG 2.2 A/AA success criteria.
| Level | Supports | Partially Supports | Does Not Support | Not Applicable |
|---|---|---|---|---|
| Level A | 19 | 0 | 0 | 12 |
| Level AA | 14 | 2 | 0 | 8 |
| Criteria | Level | Conformance Level | Remarks and Explanations |
|---|---|---|---|
| 1.1.1 Non-text Content | A | Supports | Canvas is aria-hidden; a text alternative is always present (hidden data table, natural-language summary, and embedded JSON). Whether the integrator-supplied ariaLabel/axis labels meaningfully describe the chart is an authoring judgment. (verified: automated) |
| 1.2.1 Audio-only and Video-only (Prerecorded) | A | Not Applicable | The component renders no audio/video; it draws vector marks plus a DOM overlay. (verified: automated) |
| 1.2.2 Captions (Prerecorded) | A | Not Applicable | No synchronized media. (verified: automated) |
| 1.2.3 Audio Description or Media Alternative (Prerecorded) | A | Not Applicable | No video content. (verified: automated) |
| 1.2.4 Captions (Live) | AA | Not Applicable | No live media; real-time data uses a text live region. (verified: automated) |
| 1.2.5 Audio Description (Prerecorded) | AA | Not Applicable | No video content. (verified: automated) |
| 1.3.1 Info and Relationships | A | Supports | Table/legend/surface relationships are programmatic (scoped table headers, aria-pressed legend, role="application" surface). R1 closed the last gap: the data-table x-column header now uses the configured xLabel (falls back to "x"). (verified: automated) |
| 1.3.2 Meaningful Sequence | A | Supports | DOM is appended in reading order (legend before plot; table reads x-then-series in ascending sample order); .fc-root is flex-column with no order/*-reverse/float. (verified: automated) |
| 1.3.3 Sensory Characteristics | A | Supports | Instructions name keys, not shape/position; series are identified by text name and values by axis name, not color or location. Avoiding sensory-only language in author labels remains an attestation. (verified: hybrid) |
| 1.3.4 Orientation | AA | Supports | No orientation lock; .fc-root is a fluid 100% flex-column re-measured by ResizeObserver. No @media (orientation), transform:rotate, or screen.orientation lock. (verified: automated) |
| 1.3.5 Identify Input Purpose | AA | Not Applicable | The chart component has no form inputs that collect information about the user. (verified: automated) |
| 1.4.1 Use of Color | A | Supports | Colour is never the sole differentiator. R5 auto-assigns a distinct dash pattern per series (canvas setLineDash) when the integrator gives none, and the legend swatch mirrors it (a dashed <line>, or a <rect> for area) — so two series are told apart by dash/shape, not only hue. Legend, table, and readout already give colour-free identity. The legend-semantics check asserts the swatches are mutually distinct beyond colour. (verified: automated) |
| 1.4.2 Audio Control | A | Not Applicable | The component plays no audio. (verified: automated) |
| 1.4.3 Contrast (Minimum) | AA | Partially Supports | Library DOM text passes AA on a light background (ticks 7.56:1, body 10.31:1, readout 16.98:1) and the legend hidden state no longer dims text (R8). The one residual gap is inherently integrator-dependent: the effective host background behind the tick/legend text is the host page’s, so the on-host ratio is theirs to confirm (the readout sets its own background, so it is host-independent). (verified: hybrid) |
| 1.4.4 Resize Text | AA | Supports | R7 expresses every label font (tick, axis title, legend, legend state, readout) in rem, so text-only zoom — raising the root font size without page zoom — enlarges them, and the fluid container re-measures so the chart follows. The resize-text-rem check proves the tick font scales with the root font size. (verified: automated) |
| 1.4.5 Images of Text | AA | Supports | All readable text is real DOM text (axis ticks/titles as spans, legend as buttons, table as markup). A repo-wide search confirms the canvas renders zero fillText/strokeText/measureText/ctx.font. (verified: automated) |
| 1.4.10 Reflow | AA | Supports | Fluid canvas with no library min-width, a reflowing wrap legend, and the 2-D chart-geometry exception. R7 closed the tick-overlap gap: effectiveTickCount thins tick density as the plot narrows (≥1 label per 64/28 px) so labels do not collide at ~320 CSS px or high zoom. The reflow-adaptive check proves the x-tick count drops when narrowed. Whether the integrator’s surrounding container scrolls in 2-D remains theirs. (verified: hybrid) |
| 1.4.11 Non-text Contrast | AA | Supports | The essential graphical objects — the data marks — clear 3:1: R6 added an 8-colour default palette each verified ≥3:1 on BOTH a light and a dark chart background (palette.test.ts), assigned by index when the integrator gives no colour, and the contrast-marks check re-proves each legend swatch ≥3:1 vs the documented background. Focus ring #2563eb = 5.17:1. Gridlines are decorative reference lines (not the 1.4.11 object). If the integrator overrides the palette with a low-contrast colour, that becomes their attestation. (verified: hybrid) |
| 1.4.12 Text Spacing | AA | Supports | No library text sets line-height/letter-spacing/word-spacing that would clip under a user-spacing override, and there is no overflow:hidden or fixed height on visible text (the only letter-spacing is .12em on the short uppercase axis title). (verified: automated) |
| 1.4.13 Content on Hover or Focus | AA | Supports | The readout is Hoverable (pointer-events:none) and Persistent (hidden only on pointer-leave/blur, never on a timer). R3 made it Dismissible: Escape clears readout + crosshair via dismissCursor() without moving focus. (verified: automated) |
| Criteria | Level | Conformance Level | Remarks and Explanations |
|---|---|---|---|
| 2.1.1 Keyboard | A | Supports | Cursor navigation, legend toggle, and zoom are all keyboard-operable. R2 added keyboard zoom (+/=/-/_) mirroring wheel-zoom centered on the cursor, so every pointer function now has a keyboard path. (verified: automated) |
| 2.1.2 No Keyboard Trap | A | Supports | Tab/Shift+Tab are never intercepted; onKeyDown only preventDefaults the six navigation keys. No focus() trap, aria-modal, or inert; drag pointer-capture is released on up/cancel. (verified: automated) |
| 2.1.4 Character Key Shortcuts | A | Supports | The handled key set is only ArrowRight/Left/Up/Down/Home/End plus Shift — no single letter/number/punctuation shortcut and no accesskey, so there is nothing to remap or turn off. (verified: automated) |
| 2.2.1 Timing Adjustable | A | Supports | No time limits, sessions, or countdowns. The only timers are a 150ms table-update throttle and a 100ms announce debounce — output coalescers that delay no user action. Integrators who stream data on a timer own their own pause/stop controls. (verified: hybrid) |
| 2.2.2 Pause, Stop, Hide | A | Supports | Rendering is strictly on-demand (one frame per request, no rAF recursion); no auto-motion, auto-scroll, blink, or auto-update by default. The only animation is a 0.08s readout fade, far under 5s and disabled under prefers-reduced-motion. (verified: hybrid) |
| 2.3.1 Three Flashes or Below Threshold | A | Supports | Nothing flashes: render() does a single clearRect+repaint per user-driven frame and the crosshair is a static dashed line. No @keyframes/blink/strobe; cadence is bounded by user input. A luminance-over-time probe over arbitrary author data is a human check. (verified: hybrid) |
| 2.4.1 Bypass Blocks | A | Not Applicable | Page-level; the chart is a single focus stop, not repeated page blocks. (verified: automated) |
| 2.4.2 Page Titled | A | Not Applicable | Page-level; the host owns the document title. (verified: automated) |
| 2.4.3 Focus Order | A | Supports | Focus order is legend buttons then the data surface, matching DOM/reading order; tabIndex 0 is the only focus-affecting statement (no positive tabindex, reorder, or autofocus). (verified: automated) |
| 2.4.4 Link Purpose (In Context) | A | Not Applicable | The component renders no links. (verified: automated) |
| 2.4.5 Multiple Ways | AA | Not Applicable | Page/site-level navigation concern. (verified: automated) |
| 2.4.6 Headings and Labels | AA | Supports | The component emits no section headings (host owns those) and its labels are descriptive: surface aria-label, legend group + buttons, table caption + scoped headers. R1 made the table x-column header use the configured xLabel rather than the hardcoded "x". (verified: automated) |
| 2.4.7 Focus Visible | AA | Supports | The data surface shows a :focus-visible ring, upgraded to a real outline under prefers-contrast:more and a system-color Highlight outline under forced-colors:active; legend buttons keep the native UA focus ring (no outline:none). (verified: automated) |
| 2.4.11 Focus Not Obscured (Minimum) | AA | Partially Supports | The component never fully obscures its own focused surface (the readout is a small tooltip over a fraction of the large surface). Residual gap is integrator-dependent: host-page sticky headers, toolbars, or overlays could obscure the focused chart. (verified: hybrid) |
| 2.5.1 Pointer Gestures | A | Supports | All pointer interactions are single-pointer and not path-based: drag-pan depends on the net horizontal delta (not the trajectory), zoom is wheel-driven, and no multipoint gesture exists. "Not path-based / not multipoint" is a human judgment. (verified: manual-attestation) |
| 2.5.2 Pointer Cancellation | A | Supports | No function executes on the down-event: onPointerDown only sets drag state and captures the pointer; the pan is reversible before release (computed against the immutable start domain) and finalizes on pointerup. Legend buttons activate on the up-event. (verified: automated) |
| 2.5.3 Label in Name | A | Supports | The legend buttons are the only controls with visible text labels, and each button's accessible name is computed from the same visible series-name text node it displays (swatch is aria-hidden) — verifiable by axe label-in-name. (verified: automated) |
| 2.5.4 Motion Actuation | A | Not Applicable | No device-motion-actuated functionality. (verified: automated) |
| 2.5.7 Dragging Movements | AA | Supports | R4 added pan pagers: two real ‹/› buttons that step the visible window earlier/later with a single-pointer click — the non-dragging alternative to drag-to-pan that 2.5.7 requires. They appear when the view is zoomed in (panning has an effect) and, being buttons, are keyboard- and AT-operable too. The single-pointer-pan check zooms in, then confirms the pagers are present and a click shifts the domain. (verified: automated) |
| 2.5.8 Target Size (Minimum) | AA | Supports | The data surface fills the plot inset and far exceeds 24x24. R9 gave legend buttons min-height:24px;min-width:24px;line-height:1.1, guaranteeing 24x24 regardless of host fonts — assertable by a computed-box check. (verified: automated) |
| Criteria | Level | Conformance Level | Remarks and Explanations |
|---|---|---|---|
| 3.1.1 Language of Page | A | Not Applicable | Page-level; the host sets the document language. (verified: automated) |
| 3.1.2 Language of Parts | AA | Supports | R10 made every fixed UI string (keyboard help, legend label, per-series state words, table caption, data summary) overridable via the strings option, so an integrator on a non-English page can match the document language; defaults remain English. (verified: automated) |
| 3.2.1 On Focus | A | Supports | Focusing the surface only sets cursorActive, announces the current point via the polite live region, and re-renders the crosshair in place — no focus move, navigation, window, or form submission. (verified: automated) |
| 3.2.2 On Input | A | Supports | Navigation keys move the cursor/pan in place and legend buttons toggle visibility in place via toggleSeries; no path changes context (no navigation, submission, or focus move). (verified: automated) |
| 3.2.3 Consistent Navigation | AA | Not Applicable | Cross-page navigation concern; not a single component. (verified: automated) |
| 3.2.4 Consistent Identification | AA | Supports | All legend buttons are produced by one build() routine with identical structure (swatch + name + state) and uniform aria-pressed; the surface carries a stable role="application" + aria-roledescription on every instance. (verified: automated) |
| 3.2.6 Consistent Help | A | Not Applicable | Page/site-level help mechanism concern. (verified: automated) |
| 3.3.1 Error Identification | A | Not Applicable | No form inputs / errors. (verified: automated) |
| 3.3.2 Labels or Instructions | A | Supports | Though the component has no data-entry fields, its interactive controls carry instructions: the surface aria-label embeds the full keyboard model plus a pointer to the data table, and the legend group + buttons are labeled with shown/hidden state. (verified: automated) |
| 3.3.3 Error Suggestion | AA | Not Applicable | No form inputs / errors. (verified: automated) |
| 3.3.4 Error Prevention (Legal, Financial, Data) | AA | Not Applicable | No transactions / legal commitments. (verified: automated) |
| 3.3.7 Redundant Entry | A | Not Applicable | No multi-step entry of information. (verified: automated) |
| 3.3.8 Accessible Authentication (Minimum) | AA | Not Applicable | No authentication. (verified: automated) |
| Criteria | Level | Conformance Level | Remarks and Explanations |
|---|---|---|---|
| 4.1.2 Name, Role, Value | A | Supports | Name and Role are exposed for every control. R11 closed the Value half: the focused sample is a queryable aria-describedby target (fc-active-{n}) updated in lockstep with each cursor move, and the legend state span is aria-hidden so each name stays the stable series name. (verified: automated) |
| 4.1.3 Status Messages | AA | Supports | Cursor moves not conveyed through focus are announced via a dedicated aria-live="polite" aria-atomic="true" region appended at construction, debounced 100ms. Presence + DOM-before-update ordering are automatable; that announcements are actually spoken from inside role="application" is attested. (verified: hybrid) |
| Feature | Conformance Level | Remarks and Explanations |
|---|---|---|
| prefers-reduced-motion support | Supports | The only shipped CSS transition (0.08s readout fade) is removed under prefers-reduced-motion:reduce, and the reducedMotion option is auto-detected via matchMedia and threaded into every RenderScene. There is essentially no decorative/looping motion to suppress. (verified: automated) |
| Windows High Contrast Mode (forced-colors) | Supports | R12 made the canvas participate in forced colors. The renderer probes the system palette (CanvasText/Canvas/GrayText/Highlight) when forced-colors:active and repaints the grid, axes, series, and crosshair in those colors instead of author colors; a media listener tracks live toggles. The DOM overlay already adapts and the focus ring uses Highlight. The forced-colors-canvas check confirms the bitmap actually changes when forced colors turn on. (verified: automated) |
Derived from the WCAG criteria that serve each statement — never stated independently.
| Clause | Criteria | Conformance Level | Remarks and Explanations |
|---|---|---|---|
| 4.2.1 | Usage without vision | Supports | Keyboard-operable with screen-reader announcements and a full data-table alternative. (derived from 1.1.1, 1.3.1, 2.1.1, 4.1.2, 4.1.3) |
| 4.2.2 | Usage with limited vision | Partially Supports | Resizes and reflows; some contrast/zoom aspects depend on the host background and theme. (derived from 1.4.3, 1.4.4, 1.4.10, 1.4.11) |
| 4.2.3 | Usage without perception of color | Supports | Series carry a distinct dash pattern (mirrored in the legend swatch) in addition to color, and identity is also available via the legend, data table, and readout. (derived from 1.4.1) |
| 4.2.4 | Usage without hearing | Supports | No audio content is produced. |
| 4.2.5 | Usage with limited hearing | Supports | No audio content is produced. |
| 4.2.6 | Usage without vocal capability | Supports | No speech input is required. |
| 4.2.7 | Usage with limited manipulation or strength | Supports | Full keyboard operation (including zoom), and pan pagers give a single-pointer, non-dragging alternative to drag-pan (R4) — so pointer-only and keyboard-only users are both covered. Targets meet 24×24px. (derived from 2.1.1, 2.5.1, 2.5.7, 2.5.8) |
| 4.2.8 | Usage with limited reach and strength | Supports | Interactive targets meet the 24×24px minimum. (derived from 2.5.8) |
| 4.2.9 | Minimize photosensitive seizure triggers | Supports | Nothing flashes; rendering is user-driven, not strobing. (derived from 2.3.1) |
| 4.2.10 | Usage with limited cognition, language, or learning | Supports | Predictable behavior, labeled controls with instructions, and no time limits. (derived from 3.2.1, 3.2.2, 3.3.2, 2.2.1) |
| 4.2.11 | Privacy | Supports | The component handles no personal data. |
fcharts is a UI component embedded in a host page; most software clauses are the platform/host responsibility and are marked Not Applicable.
| Clause | Criteria | Conformance Level | Remarks and Explanations |
|---|---|---|---|
| 11.5.2 | Name, role, state, value of user-interface components | Supports | The component exposes name, role, and value for its controls (derived from 4.1.2). |
| 11.6 / 11.7 | Platform accessibility services / closed functionality | Not Applicable | The chart runs in the host web browser; platform accessibility services and assistive-technology interoperability are provided by the user agent and OS, not by this component. |
| Clause | Criteria | Conformance Level | Remarks and Explanations |
|---|---|---|---|
| 12.1.1 | Accessibility and compatibility features documented | Supports | This ACR, the README accessibility section, and llms.txt document the chart’s accessibility features, keyboard model, and the localizable strings option. |
| 12.2.4 | Documentation available in an accessible electronic format | Supports | Documentation is plain Markdown/HTML, itself accessible and machine-readable. |
The following criteria rest on human attestation and must be confirmed and signed before this report is final:
This Accessibility Conformance Report is provided for informational purposes and reflects an evaluation of the named version. "VPAT" is a registered service mark of the Information Technology Industry Council (ITI). Conformance is scoped to the chart component only (see Component Scope); the embedding application and page remain responsible for page-level criteria. This document is not legal advice.
Generated 2026-06-05.