diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py
index 0b4439efde..98ac5b8208 100644
--- a/django/forms/boundfield.py
+++ b/django/forms/boundfield.py
@@ -287,6 +287,8 @@ class BoundField(RenderableFieldMixin):
attrs["required"] = True
if self.field.disabled:
attrs["disabled"] = True
+ if not widget.is_hidden and self.errors:
+ attrs["aria-invalid"] = "true"
# If a custom aria-describedby attribute is given (either via the attrs
# argument or widget.attrs) and help_text is used, the custom
# aria-described by is preserved so user can set the desired order.
diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 4d4f73d0b4..c4df99af7f 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -992,10 +992,20 @@ method you're using:
... }
>>> f = ContactForm(data, auto_id=False)
>>> print(f)
-
Subject:
This field is required.
-
Message:
-
Sender:
Enter a valid email address.
-
Cc myself:
+
Subject:
+
This field is required.
+
+
+
Message:
+
+
+
Sender:
+
Enter a valid email address.
+
+
+
Cc myself:
+
+
.. _ref-forms-error-list-format:
@@ -1154,7 +1164,7 @@ Attributes of ``BoundField``
>>> data = {"subject": "hi", "message": "", "sender": "", "cc_myself": ""}
>>> f = ContactForm(data, auto_id=False)
>>> print(f["message"])
-
+
>>> f["message"].errors
['This field is required.']
>>> print(f["message"].errors)
@@ -1166,6 +1176,13 @@ Attributes of ``BoundField``
>>> str(f["subject"].errors)
''
+ When rendering a field with errors, ``aria-invalid="true"`` will be set on
+ the field's widget to indicate there is an error to screen reader users.
+
+ .. versionchanged:: 5.0
+
+ The ``aria-invalid="true"`` was added when a field has errors.
+
.. attribute:: BoundField.field
The form :class:`~django.forms.Field` instance from the form class that
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index dad5f1bbb8..307ebb15a2 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -196,9 +196,17 @@ and the HTML output will include any validation errors:
>>> default_data = {"name": "Your name", "url": "http://"}
>>> f = CommentForm(default_data, auto_id=False)
>>> print(f)
-
Name:
-
Url:
Enter a valid URL.
-
Comment:
This field is required.
+
Name:
+
+
+
Url:
+
Enter a valid URL.
+
+
+
Comment:
+
This field is required.
+
+
This is why ``initial`` values are only displayed for unbound forms. For bound
forms, the HTML output will use the bound data.
diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt
index cceba667f9..d2265f9045 100644
--- a/docs/releases/5.0.txt
+++ b/docs/releases/5.0.txt
@@ -310,6 +310,9 @@ Forms
fields with their help text, the form field now includes the
``aria-describedby`` HTML attribute.
+* In order to improve accessibility, the invalid form field now includes the
+ ``aria-invalid="true"`` HTML attribute.
+
Generic Views
~~~~~~~~~~~~~
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index 0baddad69b..547f58632f 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -182,61 +182,70 @@ class FormsTestCase(SimpleTestCase):
str(p),
'
',
+ '',
)
def test_empty_querydict_args(self):
@@ -582,14 +591,16 @@ class FormsTestCase(SimpleTestCase):
{"email": "test@example.com", "get_spam": "False"}, auto_id=False
)
self.assertHTMLEqual(
- str(f["get_spam"]), ''
+ str(f["get_spam"]),
+ '',
)
f = SignupForm(
{"email": "test@example.com", "get_spam": "false"}, auto_id=False
)
self.assertHTMLEqual(
- str(f["get_spam"]), ''
+ str(f["get_spam"]),
+ '',
)
# A value of '0' should be interpreted as a True value (#16820)
@@ -1406,13 +1417,13 @@ class FormsTestCase(SimpleTestCase):
<em>Special</em> Field:
Something's wrong with 'Nothing to escape'
-
-
+
Special Field:
'Nothing to escape' is a safe string
+ aria-invalid="true" required>
""",
)
f = EscapingForm(
@@ -1429,13 +1440,14 @@ class FormsTestCase(SimpleTestCase):
"Something's wrong with 'Should escape < & > and "
"<script>alert('xss')</script>'"
''
- ""
+ '> and <script>alert('xss')</script>" '
+ 'aria-invalid="true" required>'
"
Special Field:
"
'
'
"
'Do not escape' is a safe string
"
'
',
+ 'value="<i>Do not escape</i>" aria-invalid="true" required>'
+ "",
)
def test_validating_multiple_fields(self):
@@ -1537,11 +1549,12 @@ class FormsTestCase(SimpleTestCase):
f.as_table(),
"""
Username:
This field is required.
-
+
+
Password1:
This field is required.
-
+
Password2:
This field is required.
-
""",
+""",
)
self.assertEqual(f.errors["username"], ["This field is required."])
self.assertEqual(f.errors["password1"], ["This field is required."])
@@ -2412,17 +2425,17 @@ class FormsTestCase(SimpleTestCase):
self.assertHTMLEqual(
p.as_ul(),
"""
This field is required.
-Username:
-
This field is required.
-Password:
""",
+Username:
This field is required.
+Password:
""",
)
p = UserRegistration({"username": ""}, auto_id=False)
self.assertHTMLEqual(
p.as_ul(),
"""