1. What is the issue? Please be detailed.
We use Central v2026.3.1 and embed @getodk/web-forms 1.1.1 (the version in central-frontend v2026.3.1) in an internal page where our team previews forms before publishing them. While testing that page with Chromium and axe-core 4.13 we found six issues. All of them reproduce with the unmodified npm build in a minimal host page, and the code involved in 1.1 to 1.5 looks unchanged on central-frontend master today. Two of them are related to open issues (getodk/web-forms#466 and #53).
1.1 A regional browser language loses to any later exact match, so the UI language is often wrong
With a form that declares no languages:
navigator.languages |
Submit button | Expected |
|---|---|---|
['es'] or ['es-PA', 'es'] |
Enviar | Enviar |
['es-PA'] or ['es-419'] |
Send | Enviar |
['fr-CA'] |
Send | Envoyer |
['en-US', 'es'] |
Enviar | Send |
['es-PA', 'fr'] |
Envoyer | Enviar |
Forms whose languages have no code (e.g. label::Español) behave the same; with a code (label::Español (es)) the UI follows the form language and is correct.
findBestLocale() tries exact matches for all candidates before it tries any base language, and resolveUILocaleCandidates() appends en to the browser languages. So for ['es-PA'] the candidates are ['es-PA', 'en'], and en matches exactly before the base language es is tried (packages/web-forms/src/lib/locale/useLocale.ts). PrimeVue's locale is chosen with the same function and candidates. Trying each candidate's base language right after the candidate itself fixes every row above: we checked it by patching the 1.1.1 build.
1.2 The form title breaks in the middle of words on narrow screens
At 360 px wide, the title "Household survey on drinking water and sanitation" is shown as "Household survey on drin" / "king water and sanitation". The title h1 has word-break: break-all (FormHeader.vue), added to fix long titles without spaces (getodk/web-forms#481). With break-all, any title that needs more than one line is cut at the last character that fits, usually in the middle of a word. With word-break: normal; overflow-wrap: anywhere instead, the title wraps at spaces, and a long title without spaces still wraps without horizontal scrolling (we checked both at 360 px).
1.3 guidance_hint is appended to the hint and always shown
A question with the hint "This is the hint." and the guidance_hint "This is the guidance hint." shows a single hint: "This is the hint.This is the guidance hint." (with no space between them). A question with only a guidance_hint shows it as a normal hint. The feature matrix doesn't mark guidance hints as supported (the row links to getodk/web-forms#53), so we expected them to be hidden, as Collect does by default. In packages/xforms-engine/src/lib/reactivity/text/createTextRange.ts, generateChunksForTranslation() adds every <value> of the itext entry as text unless its form is a media type, so <value form="guidance"> ends up in the hint. Skipping form="guidance" values until #53 is done would match Collect's default.
1.4 Group and repeat regions all have the same, empty name
axe reports landmark-unique (moderate, a best-practice rule) on our form with two labelled groups and on our form with three repeat instances; from the code, it should happen whenever two or more group or repeat panels are shown. The content of each panel is a PrimeVue Panel region (role="region") whose aria-labelledby points to the panel's toggle button. That button has no text and no aria-label (Web Forms uses its own clickable header and hides the button in FormPanel.vue), so every region's accessible name is empty. Passing the panel title to PrimeVue as toggleButtonProps.ariaLabel (or as the header prop) should be enough, e.g. "Household member 2" for a repeat instance.
1.5 The Send button doesn't meet WCAG AA contrast (getodk/web-forms#466)
White text on the primary color #3e9fcc is 2.98:1; AA needs 4.5:1, and axe reports color-contrast (serious). #466 proposes #007DB8 (4.54:1); odk-theme-preset.ts on master still has #3e9fcc. At desktop widths (we saw it at 1024 and 1280 px, not at 360 or 800 px) axe also flags the "Powered by" and version text in the footer (#64748b on #f1f5f9, 4.34:1).
1.6 select_one_external shows a free-text box
The <input query="..."> that pyxform generates for select_one_external is rendered as a plain text input, with no warning on screen or in the console, and it accepts any text. We know select_one_external isn't in the Web Forms feature matrix, but showing that the question isn't supported, instead of a text box, would avoid collecting free text by mistake.
2. What steps can we take to reproduce this issue?
- In a blank page, install
webFormsPluginand mountOdkWebFormfrom@getodk/web-forms@1.1.1(npm) with Vue, passingformXml,fetchFormAttachmentand anonSubmithandler, with no theme or locale overrides. - Convert the XLSForms below with pyxform 4.5.0 and open them in Chromium.
- For 1.1, set the browser languages (for example, Playwright's
locale: 'es-PA'), open form A and read the submit button. - For 1.2, open form A at 360 px wide.
- For 1.3 and 1.6, open forms B and D (in D, type anything in District).
- For 1.4 and 1.5, run axe-core 4.13 (WCAG 2.2 AA and best practices) on forms C, E and A at 1280 px wide.
Form A (1.1, 1.2, 1.5), with form_title "Household survey on drinking water and sanitation" in settings:
| type | name | label |
|---|---|---|
| text | respondent | What is your name? |
| select_one yes_no | consent | Do you agree to take part? |
(choices: yes_no with yes/Yes and no/No)
Form B (1.3):
| type | name | label | hint | guidance_hint |
|---|---|---|---|---|
| text | q1 | Question with hint and guidance_hint | This is the hint. | This is the guidance hint. |
| text | q2 | Question with guidance_hint only | Guidance only. |
Form C (1.4):
| type | name | label | repeat_count |
|---|---|---|---|
| begin_repeat | member | Household member | 3 |
| text | member_name | Name | |
| end_repeat |
Form D (1.6), with province in choices and district (with a province column) in external_choices:
| type | name | label | choice_filter |
|---|---|---|---|
| select_one province | province | Province | |
| select_one_external district | district | District | province=${province} |
Form E (1.4):
| type | name | label |
|---|---|---|
| begin_group | g1 | Section A |
| text | a | A |
| end_group | ||
| begin_group | g2 | Section B |
| text | b | B |
| end_group |
3. What have you tried to fix the issue?
In our page we work around 1.1 by putting es first in navigator.languages before mounting, and 1.5 by setting --p-primary-color, --p-primary-hover-color and --p-primary-active-color to the 700, 800 and 900 shades, and --odk-primary-text-color to the 700 shade (about 5.4:1 for the button). For the other four we only warn our users. We're happy to test fixes or share the test page.
4. Upload any forms or screenshots you can share publicly below.
The forms are in the tables above. We can attach the XLSForm files and screenshots if that helps.