diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md
index dce2b75c1a..390d0dd097 100644
--- a/doc/rule-descriptions.md
+++ b/doc/rule-descriptions.md
@@ -17,6 +17,7 @@
| [aria-hidden-body](https://dequeuniversity.com/rules/axe/4.0/aria-hidden-body?application=RuleDescription) | Ensures aria-hidden='true' is not present on the document body. | Critical | cat.aria, wcag2a, wcag412 | failure |
| [aria-hidden-focus](https://dequeuniversity.com/rules/axe/4.0/aria-hidden-focus?application=RuleDescription) | Ensures aria-hidden elements do not contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, wcag131 | failure, needs review |
| [aria-input-field-name](https://dequeuniversity.com/rules/axe/4.0/aria-input-field-name?application=RuleDescription) | Ensures every ARIA input field has an accessible name | Moderate, Serious | wcag2a, wcag412 | failure, needs review |
+| [aria-progressbar-name](https://dequeuniversity.com/rules/axe/4.0/aria-progressbar-name?application=RuleDescription) | Ensures every ARIA progressbar node has an accessible name | Serious | cat.aria, wcag2a, wcag111 | failure, needs review |
| [aria-required-attr](https://dequeuniversity.com/rules/axe/4.0/aria-required-attr?application=RuleDescription) | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | failure |
| [aria-required-children](https://dequeuniversity.com/rules/axe/4.0/aria-required-children?application=RuleDescription) | Ensures elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131 | failure, needs review |
| [aria-required-parent](https://dequeuniversity.com/rules/axe/4.0/aria-required-parent?application=RuleDescription) | Ensures elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131 | failure |
diff --git a/lib/rules/aria-progressbar-name.json b/lib/rules/aria-progressbar-name.json
new file mode 100644
index 0000000000..1f889fdaae
--- /dev/null
+++ b/lib/rules/aria-progressbar-name.json
@@ -0,0 +1,13 @@
+{
+ "id": "aria-progressbar-name",
+ "selector": "[role=\"progressbar\"]",
+ "matches": "aria-form-field-name-matches",
+ "tags": ["cat.aria", "wcag2a", "wcag111"],
+ "metadata": {
+ "description": "Ensures every ARIA progressbar node has an accessible name",
+ "help": "ARIA progressbar nodes must have an accessible name"
+ },
+ "all": [],
+ "any": ["aria-label", "aria-labelledby", "non-empty-title"],
+ "none": []
+}
diff --git a/test/integration/rules/aria-progressbar-name/aria-progressbar-name.html b/test/integration/rules/aria-progressbar-name/aria-progressbar-name.html
new file mode 100644
index 0000000000..ac9b08590b
--- /dev/null
+++ b/test/integration/rules/aria-progressbar-name/aria-progressbar-name.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+loading, please wait
+
+
+loading, please wait
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/integration/rules/aria-progressbar-name/aria-progressbar-name.json b/test/integration/rules/aria-progressbar-name/aria-progressbar-name.json
new file mode 100644
index 0000000000..098bb7b477
--- /dev/null
+++ b/test/integration/rules/aria-progressbar-name/aria-progressbar-name.json
@@ -0,0 +1,6 @@
+{
+ "description": "aria-progressbar-name test",
+ "rule": "aria-progressbar-name",
+ "passes": [["#pass1"], ["#pass2"], ["#pass3"]],
+ "violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"]]
+}
diff --git a/test/integration/virtual-rules/aria-progressbar-name.js b/test/integration/virtual-rules/aria-progressbar-name.js
new file mode 100644
index 0000000000..3ae7e2a8f7
--- /dev/null
+++ b/test/integration/virtual-rules/aria-progressbar-name.js
@@ -0,0 +1,89 @@
+describe('aria-progressbar-name', function() {
+ it('should pass for aria-label', function() {
+ var node = new axe.SerialVirtualNode({
+ nodeName: 'div',
+ attributes: {
+ role: 'progressbar',
+ 'aria-label': 'foobar'
+ }
+ });
+ node.parent = null;
+
+ var results = axe.runVirtualRule('aria-progressbar-name', node);
+
+ assert.lengthOf(results.passes, 1);
+ assert.lengthOf(results.violations, 0);
+ assert.lengthOf(results.incomplete, 0);
+ });
+
+ it('should incomplete for aria-labelledby', function() {
+ var node = new axe.SerialVirtualNode({
+ nodeName: 'div',
+ attributes: {
+ role: 'progressbar',
+ 'aria-labelledby': 'foobar'
+ }
+ });
+ node.parent = null;
+
+ var results = axe.runVirtualRule('aria-progressbar-name', node);
+
+ assert.lengthOf(results.passes, 0);
+ assert.lengthOf(results.violations, 0);
+ assert.lengthOf(results.incomplete, 1);
+ });
+
+ it('should pass for title', function() {
+ var node = new axe.SerialVirtualNode({
+ nodeName: 'div',
+ attributes: {
+ role: 'progressbar',
+ title: 'foobar'
+ }
+ });
+ // children are required since titleText comes after subtree text
+ // in accessible name calculation
+ node.children = [];
+ node.parent = null;
+
+ var results = axe.runVirtualRule('aria-progressbar-name', node);
+
+ assert.lengthOf(results.passes, 1);
+ assert.lengthOf(results.violations, 0);
+ assert.lengthOf(results.incomplete, 0);
+ });
+
+ it('should fail when aria-label contains only whitespace', function() {
+ var node = new axe.SerialVirtualNode({
+ nodeName: 'div',
+ attributes: {
+ role: 'progressbar',
+ 'aria-label': ' \t \n '
+ }
+ });
+ node.children = [];
+
+ var results = axe.runVirtualRule('aria-progressbar-name', node);
+
+ assert.lengthOf(results.passes, 0);
+ assert.lengthOf(results.violations, 1);
+ assert.lengthOf(results.incomplete, 0);
+ });
+
+ it('should fail when title is empty', function() {
+ var node = new axe.SerialVirtualNode({
+ nodeName: 'div',
+ attributes: {
+ role: 'progressbar',
+ title: ''
+ }
+ });
+ node.children = [];
+
+ var results = axe.runVirtualRule('aria-progressbar-name', node);
+
+ assert.lengthOf(results.passes, 0);
+ assert.lengthOf(results.violations, 1);
+ assert.lengthOf(results.incomplete, 0);
+ });
+});