#5179·axe-core

link-in-text-block: violation message does not explain why the link was matched as in-text-block

Author: blaumohnCreated Jun 18, 2026Updated Sep 19, 2026
Labelsrule metadata

Expectation

When link-in-text-block reports a violation, the message should explain why the link was considered to be inside a text block — i.e. what context triggered the rule to apply at all.

Actual

The violation message only states:

"Ensure links are distinguished from surrounding text in a way that does not rely on color."

It does not explain why the link matched the rule in the first place. The matches function (isInTextBlock) uses a length comparison: parentText.length > widgetText.length. This threshold is invisible in the output, making the result appear inconsistent or even random to the user.

Example: two HTML files with structurally identical <footer> elements — same CSS, same <a> — produce different results solely because the surrounding <span> text is one word longer in one file than in the other. The violation fires when parentText (62 chars) exceeds LINK_HREF (50 chars), and does not fire when parentText (48 chars) is shorter. Nothing in the reported message hints at this.

How to Reproduce

  1. Install dependencies:

    bash
    npm install @axe-core/playwright @playwright/test playwright
  2. Save the script below as repro.mjs and run:

    bash
    node repro.mjs
  3. Expected output:

    FAIL — parentText (span+pipe) longer than link text
    span text length : 62
    link text length : 50
    violation        : link-in-text-block
    
    PASS — parentText (span+pipe) shorter than link text
    span text length : 48
    link text length : 50
    violation        : none
javascript
import AxeBuilder from '@axe-core/playwright';
import { chromium } from '@playwright/test';

const CSS = `
  footer a { color: #555; text-decoration: none; }
  footer a:hover { text-decoration: underline; }
`;

const LINK_HREF = 'https://github.com/blaumohn/lebenslauf-web-vorlage';

const cases = [
  {
    label: 'FAIL — parentText (span+pipe) longer than link text',
    span: '2026-06-18 Dani Y. | PHP-Lebenslauf-App inkl. CI/CD, Vollstack',
  },
  {
    label: 'PASS — parentText (span+pipe) shorter than link text',
    span: '2026-06-18 Alex B. | Preview der App von Dani Y.',
  },
];

function buildPage(spanText) {
  return `<!DOCTYPE html><html lang="de"><head>
    <meta charset="utf-8">
    <style>${CSS}</style>
  </head><body>
    <main><h1>Test</h1></main>
    <footer>
      <span>${spanText}</span>
      | <a href="${LINK_HREF}" target="_blank" rel="noopener noreferrer">${LINK_HREF}</a>
    </footer>
  </body></html>`;
}

const browser = await chromium.launch();
const context = await browser.newContext();

for (const c of cases) {
  const page = await context.newPage();
  await page.setContent(buildPage(c.span));
  await page.waitForSelector('body');

  const results = await new AxeBuilder({ page })
    .withRules(['link-in-text-block'])
    .analyze();

  const v = results.violations[0];
  console.log(`\n${c.label}`);
  console.log(`span text length : ${c.span.length}`);
  console.log(`link text length : ${LINK_HREF.length}`);
  console.log(`violation        : ${v ? v.id : 'none'}`);

  await page.close();
}

await browser.close();

Additional context

The root cause is in isInTextBlock(): lib/commons/dom/is-in-text-block.js

javascript
return parentText.length > widgetText.length;

This heuristic is never surfaced in the violation message. A developer reading the report has no way to understand why one page fails and the other passes without reading lib/rules/link-in-text-block-matches.js and lib/commons/dom/is-in-text-block.js.

Suggested improvement: expose the length comparison in the violation message.

lib/commons/dom/is-in-text-block.js

diff
+  const inBlock = parentText.length > widgetText.length;
+  if (inBlock) {
+    node._axeInTextBlockLengths = {
+      parentTextLength: parentText.length,
+      widgetTextLength: widgetText.length
+    };
+  }
-  return parentText.length > widgetText.length;
+  return inBlock;

lib/checks/color/link-in-text-block-style.json

diff
-  "fail": "The link has no styling (such as underline) to distinguish it from the surrounding text"
+  "fail": {
+    "default": "The link has no styling (such as underline) to distinguish it from the surrounding text",
+    "inTextBlock": "The link has no styling to distinguish it from the surrounding text (surrounding text: ${data.parentTextLength} chars, link text: ${data.widgetTextLength} chars)"
+  }

This would make the rule transparent and the results reproducible without source-level investigation.

axe-core versions tested: 4.11.4 and 4.12.1 (latest) — both reproduce the issue.