Bug description
When a publish form fails validation, the error is shown visually but never connected to the control that failed.
Measured immediately after a failed save on an entry with an empty required field:
elements with aria-invalid="true" -> 0
inputs with aria-describedby -> 0
document.activeElement -> BODY
visible error text -> "This field is required."
toast -> role="status" aria-live="polite", "The given data was invalid."
So a screen reader user hears a generic "The given data was invalid", focus stays where it was, and nothing identifies which field is at fault. On a long blueprint that means walking the entire form to find the error.
ErrorMessage.vue renders no id, so nothing could reference it even if the wiring existed:
https://github.com/statamic/cms/blob/v6.31.0/resources/js/components/ui/ErrorMessage.vue
https://github.com/statamic/cms/blob/v6.31.0/resources/js/components/ui/Field.vue#L104 (error rendered as an unlinked sibling)
The login form has the same shape: "These credentials do not match our records." is rendered outside any live region, and the email input gets no aria-invalid.
WCAG 2.1 SC 3.3.1 Error Identification (Level A). Related: SC 3.3.2 Labels or Instructions, since field instructions are also rendered as an unlinked sibling via Description.
How to reproduce
composer create-project statamic/statamic
- Add a required field to a collection blueprint
- Create a new entry, leave the required field empty, click Save
- In the console:
document.querySelectorAll('[aria-invalid="true"]').length // 0
document.querySelectorAll('input[aria-describedby]').length // 0
document.activeElement.tagName // "BODY"
Suggested fix
- Give
ErrorMessage an id derived from the field id (${fieldId}-error).
- Set
aria-describedby and aria-invalid="true" on the control when its field has errors.
- Move focus to the first invalid control after a failed submit.
Field instructions should join the same aria-describedby list, which closes 3.3.2 in the same change.
This depends on fields reliably having ids, which is the subject of #15389.
Environment
Environment
Laravel Version: 13.30.1
PHP Version: 8.4.23
Composer Version: 2.10.2
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: en
Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED
Drivers
Broadcasting: log
Cache: file
Database: sqlite
Logs: stack / single
Mail: log
Queue: sync
Session: file
Storage
public/storage: NOT LINKED
Statamic
Addons: 0
License Key: Not set
Sites: 1
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.31.0 PRO
Installation
Fresh statamic/statamic site via CLI
Additional details
Found during a WCAG 2.1 AA audit of a vanilla composer create-project statamic/statamic install with no addons and no custom code. Tested with axe-core 4.x on Chromium 153 at 1440x1000, plus manual keyboard traversal. Source references point at v6.31.0.
Bug description
When a publish form fails validation, the error is shown visually but never connected to the control that failed.
Measured immediately after a failed save on an entry with an empty required field:
So a screen reader user hears a generic "The given data was invalid", focus stays where it was, and nothing identifies which field is at fault. On a long blueprint that means walking the entire form to find the error.
ErrorMessage.vuerenders noid, so nothing could reference it even if the wiring existed:https://github.com/statamic/cms/blob/v6.31.0/resources/js/components/ui/ErrorMessage.vue
https://github.com/statamic/cms/blob/v6.31.0/resources/js/components/ui/Field.vue#L104 (error rendered as an unlinked sibling)
The login form has the same shape: "These credentials do not match our records." is rendered outside any live region, and the email input gets no
aria-invalid.WCAG 2.1 SC 3.3.1 Error Identification (Level A). Related: SC 3.3.2 Labels or Instructions, since field
instructionsare also rendered as an unlinked sibling viaDescription.How to reproduce
composer create-project statamic/statamicSuggested fix
ErrorMessageanidderived from the field id (${fieldId}-error).aria-describedbyandaria-invalid="true"on the control when its field has errors.Field
instructionsshould join the samearia-describedbylist, which closes 3.3.2 in the same change.This depends on fields reliably having ids, which is the subject of #15389.
Environment
Installation
Fresh statamic/statamic site via CLI
Additional details
Found during a WCAG 2.1 AA audit of a vanilla
composer create-project statamic/statamicinstall with no addons and no custom code. Tested with axe-core 4.x on Chromium 153 at 1440x1000, plus manual keyboard traversal. Source references point atv6.31.0.