diff --git a/README.md b/README.md
index 33484c4067..c00551fdda 100644
--- a/README.md
+++ b/README.md
@@ -197,6 +197,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | | | |
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | | | |
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | | | |
+| [template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md) | disallow excess whitespace within words (e.g. "W e l c o m e") | | | |
| [template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md) | require non-interactive elements with aria-activedescendant to have tabindex | | 🔧 | |
| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | | | |
| [template-require-input-label](docs/rules/template-require-input-label.md) | require label for form input elements | | | |
diff --git a/docs/rules/template-no-whitespace-within-word.md b/docs/rules/template-no-whitespace-within-word.md
new file mode 100644
index 0000000000..1a8e6c0c41
--- /dev/null
+++ b/docs/rules/template-no-whitespace-within-word.md
@@ -0,0 +1,63 @@
+# ember/template-no-whitespace-within-word
+
+
+
+In practice, the predominant issue raised by inline whitespace styling is that the resultant text "formatting" is entirely visual in nature; the ability to discern the correct manner in which to read the text, and therefore, to correctly comprehend its meaning, is restricted to sighted users.
+
+Using in-line whitespace word formatting produces results that are explicitly mentioned in [WCAG's list of common sources of web accessibility failures](https://www.w3.org/TR/WCAG20-TECHS/failures.html). Specifically, this common whitespace-within-word-induced web accessibility issue fails to successfully achieve [WCAG Success Criterion 1.3.2: Meaningful Sequence](https://www.w3.org/TR/UNDERSTANDING-WCAG20/content-structure-separation-sequence.html).
+
+The `template-no-whitespace-within-word` rule operates on the assumption that artificially-spaced English words in rendered text content contain, at a minimum, two word characters fencepost-delimited by three whitespace characters (`space-char-space-char-space`) so it should be avoided.
+
+## Examples
+
+This rule **forbids** the following:
+
+```gjs
+
+ W e l c o m e
+
+```
+
+`W`**` `**`e`**` `**`l`**` `**`c`**` `**`o`**` `**`m`**` `**`e`
+
+`Wel c o me`
+
+`Wel`**` `**`c`**` `**`o`**` `**`me`
+
+```gjs
+
+ W e l c o m e
+
+ Wel c o me
+
+```
+
+This rule **allows** the following:
+
+`Welcome`
+
+`Yes`**` `**`I`**` `**`am`
+
+`It is possible to get some examples of in-word emph a sis past this rule.`
+
+`However, I do not want a rule that flags annoying false positives for correctly-used single-character words.`
+
+```gjs
+
+ Welcome
+
+ Yes I am.
+
+```
+
+This rule uses the heuristic of letter, whitespace character, letter, whitespace character, letter which makes it a good candidate for most use cases, but not ideal for some languages (such as Japanese).
+
+## Migration
+
+Use CSS to add letter-spacing to a word.
+
+## References
+
+- [F32: Using white space characters to create multiple columns in plain text content](https://www.w3.org/TR/WCAG20-TECHS/failures.html#F32)
+- [WCAG Success Criterion 1.3.2: Meaningful Sequence](https://www.w3.org/TR/UNDERSTANDING-WCAG20/content-structure-separation-sequence.html)
+- [C8: Using CSS letter-spacing to control spacing within a word](https://www.w3.org/WAI/WCAG21/Techniques/css/C8)
diff --git a/lib/rules/template-no-whitespace-within-word.js b/lib/rules/template-no-whitespace-within-word.js
new file mode 100644
index 0000000000..81b28c2fea
--- /dev/null
+++ b/lib/rules/template-no-whitespace-within-word.js
@@ -0,0 +1,117 @@
+const WHITESPACE_ENTITY_LIST = [
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '  ',
+ '',
+ '​',
+ '​',
+ '​',
+ '​',
+ '​',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ' ',
+ ' ',
+ '  ',
+ '',
+ '⁠',
+ '',
+ '⁡',
+ '⁡',
+ '',
+ '⁢',
+ '⁢',
+ '',
+ '⁣',
+ '⁣',
+];
+
+const CHARACTER_REGEX = '[a-zA-Z]';
+
+// Build a regex that catches alternating non-whitespace/whitespace characters,
+// for example, 'W e l c o m e'. The pattern requires 5 alternations to avoid
+// false positives: (whitespace)(char)(whitespace)(char)(whitespace)
+const whitespaceOrEntityRegex = `(?:\\s|${WHITESPACE_ENTITY_LIST.map(
+ (entity) => `\\${entity}`
+).join('|')})+`;
+const WHITESPACE_WITHIN_WORD_REGEX = new RegExp(
+ `${whitespaceOrEntityRegex}${CHARACTER_REGEX}${whitespaceOrEntityRegex}${CHARACTER_REGEX}${whitespaceOrEntityRegex}`
+);
+
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+ meta: {
+ type: 'layout',
+ docs: {
+ description: 'disallow excess whitespace within words (e.g. "W e l c o m e")',
+ category: 'Accessibility',
+ url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-whitespace-within-word.md',
+ templateMode: 'both',
+ },
+ schema: [],
+ messages: {
+ excessWhitespace: 'Excess whitespace in layout detected.',
+ },
+ originallyFrom: {
+ name: 'ember-template-lint',
+ rule: 'lib/rules/no-whitespace-within-word.js',
+ docs: 'docs/rule/no-whitespace-within-word.md',
+ tests: 'test/unit/rules/no-whitespace-within-word-test.js',
+ },
+ },
+
+ create(context) {
+ const sourceCode = context.getSourceCode();
+
+ return {
+ GlimmerTextNode(node) {
+ // Skip text inside attributes
+ let parent = node.parent;
+ while (parent) {
+ if (parent.type === 'GlimmerAttrNode') {
+ return;
+ }
+ // Skip text inside `,
+];
+
+const invalidHbs = [
+ {
+ code: 'W e l c o m e',
+ output: null,
+ errors: [{ message: 'Excess whitespace in layout detected.' }],
+ },
+ {
+ code: 'W e l c o m e',
+ output: null,
+ errors: [{ message: 'Excess whitespace in layout detected.' }],
+ },
+ {
+ code: 'Wel c o me',
+ output: null,
+ errors: [{ message: 'Excess whitespace in layout detected.' }],
+ },
+ {
+ code: 'Wel c o me',
+ output: null,
+ errors: [{ message: 'Excess whitespace in layout detected.' }],
+ },
+ {
+ code: '
W e l c o m e
',
+ output: null,
+ errors: [{ message: 'Excess whitespace in layout detected.' }],
+ },
+ {
+ code: 'Wel c o me
',
+ output: null,
+ errors: [{ message: 'Excess whitespace in layout detected.' }],
+ },
+ {
+ code: 'A B C ',
+ output: null,
+ errors: [{ message: 'Excess whitespace in layout detected.' }],
+ },
+];
+
+function wrapTemplate(entry) {
+ if (typeof entry === 'string') {
+ return `${entry}`;
+ }
+
+ return {
+ ...entry,
+ code: `${entry.code}`,
+ output: entry.output ? `${entry.output}` : entry.output,
+ };
+}
+
+const gjsRuleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser'),
+ parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
+});
+
+gjsRuleTester.run('template-no-whitespace-within-word', rule, {
+ valid: validHbs.map(wrapTemplate),
+ invalid: invalidHbs.map(wrapTemplate),
+});
+
+const hbsRuleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser/hbs'),
+ parserOptions: {
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ },
+});
+
+hbsRuleTester.run('template-no-whitespace-within-word', rule, {
+ valid: validHbs,
+ invalid: invalidHbs,
+});