WCAG 2.2 AA Conformance
7 min read · Documentation
WCAG 2.2 AA Conformance
The CNML web application targets Level AA of the Web Content Accessibility Guidelines (WCAG) 2.2. This page documents, criterion by criterion, how the interactive Vue islands and the Astro layout implement the relevant success criteria, and records the residual limitations that an auditor should be aware of.
The conformance scope covers the interactive signing, verification, key
management, and certificate-signing-request flows surfaced at /create,
/verify, /keys, and /csr, together with the global layout shipped in
Base.astro and the design tokens defined in app.css.
1. Perceivable
1.1.1 Non-text Content
Every icon-only and short-label control exposes an accessible name.
- The delete buttons in
KeyManager.vuecarryaria-labelvalues that interpolate the key alias, producing unambiguous names such as "Delete key NMi Certin B.V. (NL1)". - The download, cert-import, and trusted-key-removal buttons likewise expose
aria-labelvalues derived from the key alias. - The upload affordances ("Upload .pub.pem", "Import PEM", "Import cert")
carry
aria-labeldescriptions of the action. - The decorative emoji glyphs (the file icon on the dropzone, the key glyph
on empty states, the verification status glyphs) are marked
aria-hidden="true"because the adjacent text conveys the same meaning. - The OIML mark in the header has a textual equivalent in the brand lockup,
and the brand link carries
aria-label="CNML home".
1.3.1 Info and Relationships
Form fields use programmatic label association throughout.
- Every
<input>,<select>, and<textarea>inKeyManager.vue,SignDialog.vue,CsrGenerator.vue, and the form widgets is wrapped by a<label>element, or associated viafor/id(the sample-data select inSchemaForm.vue). StructuredValueEditor.vuewas refactored from an unlabelled<div>into a<fieldset>with a<legend>; the min/max, scalar, enum, and unit inputs each carry a.sr-onlylabel so that assistive technology announces the field purpose.- Required fields in the key-generation, key-import, cert-import, and CSR
identity forms are marked with a visible asterisk (
aria-hidden="true") and an.sr-only"required" string so that the constraint is announced to screen readers without being read twice. - Error messages use
role="alert"so they are announced immediately when they appear. The passphrase-strength hints inSignDialog.vueadditionally usearia-describedbyto associate the hint with its input. - Status messages in
SignDialog.vueandVerifyDrop.vueuserole="status"andaria-liveregions so that signing success, verification progress, and verification outcomes are announced politely. - The mode-switch button group in
SignDialog.vueexposes selection state viaaria-pressed, and the toggle buttons inKeyManager.vueexposearia-expandedfor their expandable forms.
1.3.5 Identify Input Purpose
Passphrase fields use autocomplete="new-password" when creating a new key
and autocomplete="current-password" when unlocking an existing one, so that
credential managers offer appropriate behaviour. The CSR identity fields use
the appropriate autofill tokens (organization, country-name,
organization title). Free-form aliases use autocomplete="off" so that
browsers do not surface irrelevant saved data.
1.4.3 Contrast (Minimum)
The design tokens defined in app.css were chosen for AA contrast. The
primary text colour (--ink, #0a1628) on the paper background (#faf6ee)
yields a ratio well above 4.5:1; the muted text colour (--ink-muted,
#6b7a92) on paper yields approximately 4.6:1, satisfying the threshold for
body text. The accent colour (#004996) on paper yields approximately 7.4:1.
The status badges and callouts are defined as paired
background-plus-foreground tokens, each tuned for AA contrast, and flip to
dark-mode equivalents under .dark.
1.4.10 Reflow
The layout uses a fluid max-width container (max-w-7xl) and the forms use
responsive grid templates (grid-cols-1 sm:grid-cols-2) that collapse to a
single column below the small breakpoint. No fixed-width container prevents
reflow at 320 CSS pixels; the only horizontally-oriented regions (the
verification tile grid, the key-row action button clusters) use
flex-wrap so that they reflow rather than truncate.
1.4.11 Non-text Contrast
The focus indicator added in global.css (a 2-pixel solid outline in the
accent colour at 2-pixel offset) meets the 3:1 contrast threshold against the
paper, paper-soft, and paper-raised surfaces. Button borders on the secondary
variant use --rule against paper, which is at the threshold; the hover and
focus-visible states raise the border to --ink and --accent respectively,
clearing 3:1.
1.4.12 Text Spacing
No component declares a fixed line-height that would break when a user
overrides spacing. The form labels use a relative font size and the inputs
inherit body line-height. The cnml-code-pane uses a unitless line-height of
1.5, which scales with user font-size overrides.
2. Operable
2.1.1 Keyboard
Every interactive control is operable from the keyboard.
- All buttons, links, and form controls are native HTML elements and inherit keyboard operability.
- The
VerifyDrop.vuedropzone was extended with a@keydownhandler that activates the file picker on Enter and Space, in addition to the native<label>-wrapped file input that is now in the tab order (.sr-onlyrather thanhidden, so it remains focusable). - The
SignDialog.vuemode-switch buttons are native<button>elements and are reachable by Tab.
2.1.2 No Keyboard Trap
SignDialog.vue implements a focus trap: while the dialog is open, Tab and
Shift-Tab cycle within the dialog's focusable elements. Escape closes the
dialog. On close, focus is restored to the element that had focus before the
dialog opened (lastFocused). The focus trap is implemented in the onKey
handler and the watch on props.open.
2.4.1 Bypass Blocks
The skip-link in Base.astro (<a href="#main">) is the first focusable
element in the document. It is visually hidden until focused, at which point
it appears at the top-left of the viewport. The <main> element carries
id="main" as the jump target.
2.4.3 Focus Order
No tabindex value greater than 0 is used anywhere in the application. The
dropzone's tabindex="0" places it in the natural tab order at its document
position, which matches the visual order. The dialog's focus management
explicitly moves focus to the first focusable child on open.
2.4.7 Focus Visible
The global :focus-visible style in app.css applies a 2-pixel accent
outline to every focusable element. The component-specific :focus-visible
rules added to global.css reinforce this for buttons, nav links, and the
brand lockup, and the dropzone's focus-visible: utilities apply a visible
border and outline when activated by keyboard.
2.5.5 Target Size (Minimum)
The .cnml-btn class declares min-height: 1.5rem and default padding of
0.5rem 1rem, yielding a target of at least 24 by 24 CSS pixels. The
Counter component's buttons are 32 by 32 pixels. The dropzone's hit area is
the entire bordered region.
3. Understandable
3.2.1 On Focus and 3.2.2 On Input
No control initiates a context change on focus or on input. The sample-data
select in SchemaForm.vue changes form contents on change, but this is an
explicit user action on a <select> whose options are visible, and the
change is to the form's own contents rather than a navigation or context
change.
3.3.1 Error Identification and 3.3.3 Error Suggestion
All errors are described in text. The passphrase-strength hints in
SignDialog.vue go beyond color: they include the count of remaining
characters required ("Too short - needs 2 more characters"). The
passphrase-mismatch hint states "Doesn't match." Key-generation errors,
cert-import errors, CSR errors, and trust-store errors are surfaced as text
in .cnml-text-danger callouts with role="alert". The color is never the
only signal.
4. Robust
4.1.2 Name, Role, Value
SignDialog.vueexposesrole="dialog",aria-modal="true", andaria-labelledbypointing to the dialog title.- The
VerifyDrop.vuedropzone exposesrole="button"with anaria-labeldescribing the affordance. - The mode-switch in
SignDialog.vueexposesaria-pressedfor each toggle. - The expandable-form toggles in
KeyManager.vueexposearia-expanded. - The verification status tiles expose
role="status"and an.sr-onlytext equivalent of the glyph. - The
StructuredValueEditor.vuewidget uses<fieldset>/<legend>for its grouping semantics.
Reduced Motion
global.css includes a @media (prefers-reduced-motion: reduce) block that
collapses every animation and transition to a near-zero duration and forces
scroll-behavior: auto. This honors the OS-level "reduce motion" preference
and ensures that the card-hover lift, button color transitions, and smooth
scrolling do not present a vestibular hazard.
Known Limitations
The following items are out of scope for this pass and are recorded for a future audit:
- The dynamically-created file inputs in
KeyManager.vue(triggerCertUpload,triggerPrivateKeyUpload,triggerTrustedKeyUpload) are spawned viadocument.createElementand invoked with.click()immediately. They are not in the tab order; the visible button labels serve as the accessible affordance. A future enhancement could replace these with persistent visually-hidden file inputs. - The
<details>/<summary>disclosure pattern used for live JSON previews and signed-XML previews relies on the browser's native disclosure triangle, which does not always meet 3:1 contrast. This is a browser-rendered affordance outside the application's style scope. - The verification check pipeline emits check results with
role="status"on each tile; a future enhancement could consolidate these into a singlearia-livesummary region to reduce announcement verbosity for screen-reader users verifying many files in sequence. - Color contrast for the
--ink-mutedtoken (#6b7a92) on the cream paper background is at the AA threshold (approximately 4.6:1) for body text but does not satisfy the 7:1 AAA threshold. This is acceptable at the AA target but should be revisited if the target is raised.