Skip to main content

Accessibility

The widget is built to WCAG 2.1 AA and the suite proves it rather than asserting it: a full axe sweep over every widget state in both view trees, a keyboard-only run through the entire report flow with a focus assertion at every transition, and contrast measured on the values the browser actually rendered.

What that means in practice for the plain tree: the modal is a native <dialog>, so the browser owns the focus trap, Escape, and focus return; every control the widget renders — the floating button and everything in the panel — is at least 44×44 CSS px; no field renders below 16px, so iOS Safari never zooms the page in on focus and leaves the reporter magnified; status and error regions are live regions that announce without stealing focus; and focus is never dropped on <body> when a control disappears mid-flow.

A rejected field is marked on the control itself, not only in the shared alert region: it carries aria-invalid, points at the message with aria-describedby, and shows a visible error border. So a reporter who tabs back through the form after a rejection finds the field that failed instead of eight that sound equally valid.

That marking is narrower than the focus jump, and the difference is deliberate. A listener veto, a rate limit and a switched-off widget all move focus to the message box while the text the reporter wrote is perfectly fine — marking it there would announce that the message is invalid because the operator turned the form off. Only a real field rejection marks a control, plus the privacy acknowledgment, which is the one non-field rejection the reporter can fix where they stand.

The one control that is not covered by that sentence is the standalone <x-visual-feedback::trigger>, and deliberately so: it ships unstyled so your own button classes apply to it, which means its size is yours to set. See Placing the trigger.

The plain tree has no motion at all — no transitions, no animations — so there is nothing for a reduced-motion preference to switch off.

The WireKit tree inherits WireKit, which is the point of it: its controls are WireKit components, so their size, type scale and motion are your design system's, not this package's.

Since WireKit 2.21 that tree holds the same floors the plain one does. Measured in a touch profile, every field, select, textarea and button in the panel is at least 44×44 CSS px and every text-entry control is at 16px, so iOS Safari does not zoom the viewport on focus. WireKit scopes those floors to a coarse pointer, which is why a desktop measurement still shows the smaller design token — that is intended, not a gap.

That release also brought WireKit's motion under prefers-reduced-motion, which closes the last difference between the trees: the plain one has no motion to switch off, and the WireKit one now switches its own off. Measured with the preference emulated, transitions inside the panel collapse to 1ms — the value WireKit chose deliberately, because at 0s an entrance animation can leave its element in the DOM for good.

Every control in both trees clears the target, WireKit's modal close button included — from WireKit 2.24. Below that version it was the one exception: 32 CSS px, clearing the 24px AA minimum but not the 44×44 AAA target, and carrying neither of the classes WireKit's touch floor applies to. It was reported upstream rather than overridden here, because overriding a design system's dialog chrome would make the widget the one thing on the page ignoring its own tokens — and 2.24 fixed it the right way: the visible box stays 32px and a centered transparent 44×44 hit area sits over it, so nothing about the render changed.

On earlier WireKit versions the whole panel sat at 40px controls and a 14px type scale. If you are pinned below 2.21 and those floors matter to you, use the plain tree.

Three things are your page's job, not the widget's, and a widget that supplied them would be the defect:

  • <meta name="viewport" content="width=device-width, initial-scale=1"> — without it mobile browsers lay your page out at 980px and nothing responsive works, here or anywhere else on the page.
  • A lang attribute on <html>. The widget renders in the application's locale and its own strings are translated into seven, but the language of the document is declared once by the page, and a screen reader picks its voice from that one declaration.
  • A <main> landmark and one <h1>. Automated accessibility checks report both against the document, and the widget cannot add them to a page it does not own.

The installation snippet carries all three, which is why it is written as a whole layout rather than as the three lines that belong to this package.