diff --git a/README.md b/README.md
index 02ce6578..14aedd58 100644
--- a/README.md
+++ b/README.md
@@ -82,27 +82,28 @@ This config will be interpreted in the following way:
π§ Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
β Deprecated.
-| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β | Description | πΌ | π§ | β |
-| :------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
-| [a11y-no-generic-link-text](docs/rules/a11y-no-generic-link-text.md) | disallow generic link text | | | β |
-| [array-foreach](docs/rules/array-foreach.md) | enforce `for..of` loops over `Array.forEach` | β
| | |
-| [async-currenttarget](docs/rules/async-currenttarget.md) | disallow `event.currentTarget` calls inside of async functions | π | | |
-| [async-preventdefault](docs/rules/async-preventdefault.md) | disallow `event.preventDefault` calls inside of async functions | π | | |
-| [authenticity-token](docs/rules/authenticity-token.md) | disallow usage of CSRF tokens in JavaScript | π | | |
-| [get-attribute](docs/rules/get-attribute.md) | disallow wrong usage of attribute names | π | π§ | |
-| [js-class-name](docs/rules/js-class-name.md) | enforce a naming convention for js- prefixed classes | π | | |
-| [no-blur](docs/rules/no-blur.md) | disallow usage of `Element.prototype.blur()` | π | | |
-| [no-d-none](docs/rules/no-d-none.md) | disallow usage the `d-none` CSS class | π | | |
-| [no-dataset](docs/rules/no-dataset.md) | enforce usage of `Element.prototype.getAttribute` instead of `Element.prototype.datalist` | π | | |
-| [no-dynamic-script-tag](docs/rules/no-dynamic-script-tag.md) | disallow creating dynamic script tags | β
| | |
-| [no-implicit-buggy-globals](docs/rules/no-implicit-buggy-globals.md) | disallow implicit global variables | β
| | |
-| [no-inner-html](docs/rules/no-inner-html.md) | disallow `Element.prototype.innerHTML` in favor of `Element.prototype.textContent` | π | | |
-| [no-innerText](docs/rules/no-innerText.md) | disallow `Element.prototype.innerText` in favor of `Element.prototype.textContent` | π | π§ | |
-| [no-then](docs/rules/no-then.md) | enforce using `async/await` syntax over Promises | β
| | |
-| [no-useless-passive](docs/rules/no-useless-passive.md) | disallow marking a event handler as passive when it has no effect | π | π§ | |
-| [prefer-observers](docs/rules/prefer-observers.md) | disallow poorly performing event listeners | π | | |
-| [require-passive-events](docs/rules/require-passive-events.md) | enforce marking high frequency event handlers as passive | π | | |
-| [role-supports-aria-props](docs/rules/role-supports-aria-props.md) | Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`. | βοΈ | | |
-| [unescaped-html-literal](docs/rules/unescaped-html-literal.md) | disallow unescaped HTML literals | π | | |
+| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β | Description | πΌ | π§ | β |
+| :----------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
+| [a11y-aria-label-is-well-formatted](docs/rules/a11y-aria-label-is-well-formatted.md) | [aria-label] text should be formatted as you would visual text. | βοΈ | | |
+| [a11y-no-generic-link-text](docs/rules/a11y-no-generic-link-text.md) | disallow generic link text | | | β |
+| [array-foreach](docs/rules/array-foreach.md) | enforce `for..of` loops over `Array.forEach` | β
| | |
+| [async-currenttarget](docs/rules/async-currenttarget.md) | disallow `event.currentTarget` calls inside of async functions | π | | |
+| [async-preventdefault](docs/rules/async-preventdefault.md) | disallow `event.preventDefault` calls inside of async functions | π | | |
+| [authenticity-token](docs/rules/authenticity-token.md) | disallow usage of CSRF tokens in JavaScript | π | | |
+| [get-attribute](docs/rules/get-attribute.md) | disallow wrong usage of attribute names | π | π§ | |
+| [js-class-name](docs/rules/js-class-name.md) | enforce a naming convention for js- prefixed classes | π | | |
+| [no-blur](docs/rules/no-blur.md) | disallow usage of `Element.prototype.blur()` | π | | |
+| [no-d-none](docs/rules/no-d-none.md) | disallow usage the `d-none` CSS class | π | | |
+| [no-dataset](docs/rules/no-dataset.md) | enforce usage of `Element.prototype.getAttribute` instead of `Element.prototype.datalist` | π | | |
+| [no-dynamic-script-tag](docs/rules/no-dynamic-script-tag.md) | disallow creating dynamic script tags | β
| | |
+| [no-implicit-buggy-globals](docs/rules/no-implicit-buggy-globals.md) | disallow implicit global variables | β
| | |
+| [no-inner-html](docs/rules/no-inner-html.md) | disallow `Element.prototype.innerHTML` in favor of `Element.prototype.textContent` | π | | |
+| [no-innerText](docs/rules/no-innerText.md) | disallow `Element.prototype.innerText` in favor of `Element.prototype.textContent` | π | π§ | |
+| [no-then](docs/rules/no-then.md) | enforce using `async/await` syntax over Promises | β
| | |
+| [no-useless-passive](docs/rules/no-useless-passive.md) | disallow marking a event handler as passive when it has no effect | π | π§ | |
+| [prefer-observers](docs/rules/prefer-observers.md) | disallow poorly performing event listeners | π | | |
+| [require-passive-events](docs/rules/require-passive-events.md) | enforce marking high frequency event handlers as passive | π | | |
+| [role-supports-aria-props](docs/rules/role-supports-aria-props.md) | Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`. | βοΈ | | |
+| [unescaped-html-literal](docs/rules/unescaped-html-literal.md) | disallow unescaped HTML literals | π | | |
diff --git a/docs/rules/a11y-aria-label-is-well-formatted.md b/docs/rules/a11y-aria-label-is-well-formatted.md
new file mode 100644
index 00000000..9c2f164a
--- /dev/null
+++ b/docs/rules/a11y-aria-label-is-well-formatted.md
@@ -0,0 +1,39 @@
+# [aria-label] text should be formatted as you would visual text (`github/a11y-aria-label-is-well-formatted`)
+
+πΌ This rule is enabled in the βοΈ `react` config.
+
+
+
+## Rule Details
+
+`[aria-label]` content should be formatted in the same way you would visual text. Please use sentence case.
+
+Do not connect the words like you would an ID. An `aria-label` is not an ID, and should be formatted as human-friendly text.
+
+## Resources
+
+- [Using aria-label](https://www.w3.org/WAI/tutorials/forms/labels/#using-aria-label)
+
+## Examples
+
+### **Incorrect** code for this rule π
+
+```html
+
+```
+
+```html
+
+```
+
+### **Correct** code for this rule π
+
+```html
+
+```
+
+```html
+
+```
+
+## Version
diff --git a/lib/configs/react.js b/lib/configs/react.js
index a49ead99..aa036960 100644
--- a/lib/configs/react.js
+++ b/lib/configs/react.js
@@ -9,6 +9,7 @@ module.exports = {
extends: ['plugin:jsx-a11y/recommended'],
rules: {
'jsx-a11y/role-supports-aria-props': 'off', // Override with github/role-supports-aria-props until https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/910 is resolved
+ 'github/a11y-aria-label-is-well-formatted': 'error',
'github/role-supports-aria-props': 'error',
'jsx-a11y/no-aria-hidden-on-focusable': 'error',
'jsx-a11y/anchor-ambiguous-text': [
diff --git a/lib/index.js b/lib/index.js
index 025b23ff..c4a8b3ee 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,6 +1,7 @@
module.exports = {
rules: {
'a11y-no-generic-link-text': require('./rules/a11y-no-generic-link-text'),
+ 'a11y-aria-label-is-well-formatted': require('./rules/a11y-aria-label-is-well-formatted'),
'array-foreach': require('./rules/array-foreach'),
'async-currenttarget': require('./rules/async-currenttarget'),
'async-preventdefault': require('./rules/async-preventdefault'),
diff --git a/lib/rules/a11y-aria-label-is-well-formatted.js b/lib/rules/a11y-aria-label-is-well-formatted.js
new file mode 100644
index 00000000..3e9ace86
--- /dev/null
+++ b/lib/rules/a11y-aria-label-is-well-formatted.js
@@ -0,0 +1,31 @@
+const {getProp} = require('jsx-ast-utils')
+
+module.exports = {
+ meta: {
+ docs: {
+ description: '[aria-label] text should be formatted as you would visual text.',
+ url: require('../url')(module),
+ },
+ schema: [],
+ },
+
+ create(context) {
+ return {
+ JSXOpeningElement: node => {
+ const prop = getProp(node.attributes, 'aria-label')
+ if (!prop) return
+
+ const propValue = prop.value
+ if (propValue.type !== 'Literal') return
+
+ const ariaLabel = propValue.value
+ if (ariaLabel.match(/^[a-z]+.*$/)) {
+ context.report({
+ node,
+ message: '[aria-label] text should be formatted the same as you would visual text. Use sentence case.',
+ })
+ }
+ },
+ }
+ },
+}
diff --git a/tests/a11y-aria-label-is-well-formatted.js b/tests/a11y-aria-label-is-well-formatted.js
new file mode 100644
index 00000000..cf244a7e
--- /dev/null
+++ b/tests/a11y-aria-label-is-well-formatted.js
@@ -0,0 +1,31 @@
+const rule = require('../lib/rules/a11y-aria-label-is-well-formatted')
+const RuleTester = require('eslint').RuleTester
+
+const ruleTester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+})
+
+const errorMessage = '[aria-label] text should be formatted the same as you would visual text. Use sentence case.'
+
+ruleTester.run('a11y-aria-label-is-well-formatted', rule, {
+ valid: [
+ {code: "Read more;"},
+ {code: "Read more;"},
+ {code: ";"},
+ {code: ";"},
+ {code: ";"},
+ {code: 'Read more'},
+ ],
+ invalid: [
+ {code: ";", errors: [{message: errorMessage}]},
+ {code: ";", errors: [{message: errorMessage}]},
+ {code: ";", errors: [{message: errorMessage}]},
+ {code: ";", errors: [{message: errorMessage}]},
+ ],
+})