Rule handbook metadata

Rule
DET.CTA.LABEL_NONEMPTY · lane deterministic
Page status
stale
page_version
c1fec213f1e4fe30dae16656019259037d7301d5b562d51489d35f33105b5de9
generated_at
2026-05-19T21:45:00.000Z
registry_fingerprint
6773fda516344e110b5a7b1435e655e1264e773825ca8bbe62194189891c42ba

How this rule is fixed

Pilot deterministic fixer: handbook_after · harness mode standalone · verify expect_rule_clean.

  • Harness: apply-harness-fixture-remediation.py copies the After example HTML from this handbook page onto the defect fixture, then invoke-det-ruleset-remediation-verify.sh re-audits and expects zero findings for this rule.
  • Production sites: run-website-ux-remediation-loop.sh runs handbook_html_patch (HTML patches in lib/ux-deterministic-fixers/fixers/patches/) before invoking the Cursor agent when the quality gate still fails.

Detection module: docs/design/ux-audit/deterministic-design-rules.md#det-cta-label_nonempty. Scroll down for Before / After examples and Evidence and remediation steps.

Purpose

Kitchen Sink heroes, cards, and doc chrome expose conversion and navigation through <a class="btn …">, <button>, input[type=button|submit|reset|image], and elements with role="button" or role="link" (components/components.py, components/layouts.py). Screen readers, voice control, and the UX auditor all resolve an accessible name for each visible control. When that name is empty—icon-only markup with no aria-label, whitespace-only link text, or a submit input with no value—users cannot identify the action and automated checks flag a major accessibility defect.

This deterministic rule scans visible interactive controls in the metrics phase (collectCtaLabelNonemptyReport), requires at least MIN_CTA_LABEL_CHARS (1) trimmed character in the computed accessible name, and skips nested controls inside another interactive ancestor, hidden subtrees (aria-hidden, display:none), and inert nodes. Names may come from aria-label, aria-labelledby, visible text, title (when non-empty), input value/alt, descendant img[alt], or .sr-only / .visually-hidden helper text.

Plan: Crawl hero, card footer, and mobile nav URLs. Do: Keep human-readable labels on every .btn; add aria-label for icon-only doc nav toggles. Check: metrics.ctaLabelNonemptyReport.violations is empty. Adjust: Replace placeholder-only CTAs and icon glyphs without names before tuning hierarchy or button-group caps.

Passing signals

  • landing-hero-actions__buttons anchors include visible text: btn btn-forge + btn btn-cyan-outline with labels such as “Start quickstart” and “Read docs”.
  • Icon-only controls (mobile offcanvas opener, toolbar glyphs) set aria-label="Open navigation" (or equivalent) on the <button class="btn btn-forge …"> itself.
  • Card footers use <a class="btn btn-forge" href="…">Read the guide</a> or btn btn-forge-outline with non-empty link text—not bare icons.
  • aria-labelledby points at visible heading or caption text when the control surface is minimal.
  • input type="submit" / type="button" ship a non-empty value; type="image" includes meaningful alt.
  • metrics.ctaLabelNonemptyReport reports violations: [] after crawl; evidence does not contain empty_accessible_name.

Failing signals

  • kind: empty-accessible-name on a visible <a class="btn btn-forge" href="…"> with no text, aria-label, or qualifying aria-labelledby reference.
  • Icon-only <button class="btn btn-forge"> containing only a decorative glyph span, with no aria-label and no .sr-only helper inside the control.
  • Whitespace-only link text that normalizes to fewer than one character after trim.
  • href="#" anchors counted as interactive only when role="button" or role="link" is set—still require a non-empty name when visible.
  • Evidence strings like empty_accessible_name tag="a"="a.btn-forge" or tag="button"="button.btn-forge" with major severity.
  • Page passes DET.CTA.HIERARCHY or DET.BUTTON.GROUP.MAX yet fails here because naming, not count or class role, is the gate.

Before example

Before (failing example)

Governed human + agent delivery

One methodology spine from intent to evidence.

Failing KS markup: hero primary with no visible text and a mobile nav toggle with icon-only content—both resolve to an empty accessible name.

After example

After (passing example)

Governed human + agent delivery

One methodology spine from intent to evidence.

Passing KS markup: same layout with aria-label on the icon control and visible hero button text.

Evidence and remediation

Evidence: Auditor field metrics.ctaLabelNonemptyReport with minLabelChars: 1, controlCount, and violations[] (kind: empty-accessible-name, tag, selectorHint, optional role / href). Findings cite empty_accessible_name and the control hint (for example a.btn-forge or button.btn-forge[@Ldg]). Reproduce in DevTools by selecting each visible .btn / button and inspecting the accessibility tree name.

Remediate (in order):

  1. Add visible link or button text to every hero and card CTA (primary_cta_label, secondary_cta_label in generators—never emit empty strings).
  2. For icon-only controls, set aria-label on the interactive element (match components/layouts.py: aria-label="Open navigation").
  3. Prefer aria-labelledby when a visible heading already names the action; ensure referenced nodes are not aria-hidden.
  4. For input type="submit" / type="button", set a non-empty value; for type="image", set meaningful alt.
  5. Re-run analyze-website-ux.mjs metrics on affected URLs; reconcile with DET.CTA.HIERARCHY and AI.CREDIBILITY.NO_OVERCLAIM so labels stay specific, not generic filler.