Problem
I'm developing an accessible form in Vue with proper form validation. When a user submits an invalid form, I want to:
- Announce all validation errors to screen reader users
- Move focus to the first invalid field
However, the current implementation causes confusion for screen reader users. When focus moves to the invalid field, the screen reader announces Firstly, the field's own error message and its aria labels (automatically when focus moves) then my custom announcement with all form errors
This creates a redundant and confusing experience where the same error is announced twice but in different contexts.
Current Implementation
I'm using a combination of form validation, error collection, and programmatic focus management. When validation fails:
- I collect all error messages
- Announce them via an aria-live region
- Move focus to the first invalid field
But with this implementation it is not working in order. I want to have the errors announced first and then the trigger the current field announcements like mentioned with aria attributes.
The problematic code:
const submit = async (event) => {
event.preventDefault();
const { valid: isValid } = await myForm.value.validate();
if (!isValid) {
// Get all form items with errors
const formItems = myForm.value.items;
// Collect all error messages
const errors = formItems.reduce((acc, item) => {
if (item.errorMessages?.length > 0) {
acc.push(...item.errorMessages);
}
return acc;
}, []);
// Announce all errors
if (errors.length > 0) {
announceToScreenReader(`Please correct the following errors: ${errors.join('. ')}`);
}
// Find and focus the first invalid field
const firstInvalidField = formItems.find(item =>
item.errorMessages?.length > 0
);
if (firstInvalidField) {
const inputElement = document.getElementById(firstInvalidField.id);
if (inputElement) {
inputElement.focus();
inputElement.scrollIntoView({ behavior: 'smooth' });
}
}
}
};
const announceToScreenReader = (message) => {
const announcer = document.getElementById('sr-announcer');
if (!announcer) {
const newAnnouncer = document.createElement('div');
newAnnouncer.id = 'sr-announcer';
newAnnouncer.setAttribute('aria-live', 'polite');
newAnnouncer.setAttribute('aria-atomic', 'true');
newAnnouncer.classList.add('sr-only');
document.body.appendChild(newAnnouncer);
setTimeout(() => {
newAnnouncer.textContent = message;
}, 100);
} else {
announcer.textContent = '';
setTimeout(() => {
announcer.textContent = message;
}, 100);
}
};
And the template
<v-form ref="myForm" @submit.prevent="submit">
<v-text-field
v-model="firstName"
:rules="[nameValidation.required, nameValidation.min]"
:aria-label="'First Name'"
>
<template #message="arg">
<div aria-live="off" id="firstNameError">{{ arg.message }}</div>
</template>
</v-text-field>
<!-- Other form fields... -->
<v-btn type="submit">Submit</v-btn>
</v-form>
How can I properly sequence these announcements so that:
The screen reader first announces the complete list of errors (via the aria-live region), Then trigger announcements for first invalid field which is focused
Problem
I'm developing an accessible form in Vue with proper form validation. When a user submits an invalid form, I want to:
- Announce all validation errors to screen reader users
- Move focus to the first invalid field
However, the current implementation causes confusion for screen reader users. When focus moves to the invalid field, the screen reader announces Firstly, the field's own error message and its aria labels (automatically when focus moves) then my custom announcement with all form errors
This creates a redundant and confusing experience where the same error is announced twice but in different contexts.
Current Implementation
I'm using a combination of form validation, error collection, and programmatic focus management. When validation fails:
- I collect all error messages
- Announce them via an aria-live region
- Move focus to the first invalid field
But with this implementation it is not working in order. I want to have the errors announced first and then the trigger the current field announcements like mentioned with aria attributes.
The problematic code:
const submit = async (event) => {
event.preventDefault();
const { valid: isValid } = await myForm.value.validate();
if (!isValid) {
// Get all form items with errors
const formItems = myForm.value.items;
// Collect all error messages
const errors = formItems.reduce((acc, item) => {
if (item.errorMessages?.length > 0) {
acc.push(...item.errorMessages);
}
return acc;
}, []);
// Announce all errors
if (errors.length > 0) {
announceToScreenReader(`Please correct the following errors: ${errors.join('. ')}`);
}
// Find and focus the first invalid field
const firstInvalidField = formItems.find(item =>
item.errorMessages?.length > 0
);
if (firstInvalidField) {
const inputElement = document.getElementById(firstInvalidField.id);
if (inputElement) {
inputElement.focus();
inputElement.scrollIntoView({ behavior: 'smooth' });
}
}
}
};
const announceToScreenReader = (message) => {
const announcer = document.getElementById('sr-announcer');
if (!announcer) {
const newAnnouncer = document.createElement('div');
newAnnouncer.id = 'sr-announcer';
newAnnouncer.setAttribute('aria-live', 'polite');
newAnnouncer.setAttribute('aria-atomic', 'true');
newAnnouncer.classList.add('sr-only');
document.body.appendChild(newAnnouncer);
setTimeout(() => {
newAnnouncer.textContent = message;
}, 100);
} else {
announcer.textContent = '';
setTimeout(() => {
announcer.textContent = message;
}, 100);
}
};
And the template
<v-form ref="myForm" @submit.prevent="submit">
<v-text-field
v-model="firstName"
:rules="[nameValidation.required, nameValidation.min]"
:aria-label="'First Name'"
>
<template #message="arg">
<div aria-live="off" id="firstNameError">{{ arg.message }}</div>
</template>
</v-text-field>
<!-- Other form fields... -->
<v-btn type="submit">Submit</v-btn>
</v-form>
How can I properly sequence these announcements so that:
The screen reader first announces the complete list of errors (via the aria-live region), Then trigger announcements for first invalid field which is focused
Share Improve this question asked Mar 8 at 10:13 user20042604user20042604 731 silver badge8 bronze badges1 Answer
Reset to default 0I don't think there is a way you can do this reliably. As soon as you manually put focus on the invalid input, the screen reader is going to stop announcing your list of errors in the live region and announce the input you focused. You could put a delay on the focus to give the screen reader a chance to announce all of the errors in the live region, but then you'll be guessing how long that will take.
One common method for announcing a list of errors is to move focus to the list and make each error message a link to its input. You could also not move the focus at all and just add the errors to the live region. Also, creating a master list of errors is not required. You can just add the error messages to their inputs and put focus on the first invalid input. If you are going to do a master list then I would recommend you move focus to it and add links to the inputs.