Skip to content

Validation errors are not programmatically associated with the field that failed #15396

Description

@bpmore

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

  1. composer create-project statamic/statamic
  2. Add a required field to a collection blueprint
  3. Create a new entry, leave the required field empty, click Save
  4. In the console:
document.querySelectorAll('[aria-invalid="true"]').length   // 0
document.querySelectorAll('input[aria-describedby]').length // 0
document.activeElement.tagName                              // "BODY"

Suggested fix

  1. Give ErrorMessage an id derived from the field id (${fieldId}-error).
  2. Set aria-describedby and aria-invalid="true" on the control when its field has errors.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions