Rule handbook metadata

Rule
DET.APP.FOCUS_TRAP · lane deterministic
Page status
stale
page_version
b00d291ded12c54043f412b186dfb0ed74880144c3ffab69d19386d2c6be1b47
generated_at
2026-05-19T20:15: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-app-focus-trap. Scroll down for Before / After examples and Evidence and remediation steps.

Purpose

Kitchen Sink handbook and product layouts rely on overlay shells that block the page until the user dismisses them:

  • Bootstrap.modal and .offcanvas (mobile handbook nav docNavOffcanvas, Kco).
  • KS diagram lightbox#diagramModal with .diagram-modal-backdrop, .diagram-modal-close, and closeDiagramModal() (forge-theme.js).
  • Topic preview#topicPreviewModal with .topic-preview-modal-backdrop, [data-topic-preview-close], and openTopicPreviewModal() / closeTopicPreviewModal().
  • Custom dialogs[role="dialog"] surfaces with aria-modal="true".

While an overlay is open, keyboard users must not tab into background <main> content, lose focus outside the shell, or be stuck without a dismiss control. This deterministic rule scans overlay markup on every crawled page and, when a safe opener exists, runs a short Tab-cycle probe (up to two shells per page).

Plan: Inventory .modal, .offcanvas, #diagramModal, and #topicPreviewModal on app/handbook templates. Do: Pair shells with visible openers, Bootstrap dismiss wiring, and KS close affordances. Check: Open the overlay with keyboard, Tab through controls, confirm focus never lands on main links. Adjust: Add aria-modal="true", inert/aria-hidden on the page root, or Bootstrap's built-in trap when custom JS bypasses it.

Passing signals

  • Every overlay shell includes a keyboard-reachable dismiss: data-bs-dismiss, .btn-close, .diagram-modal-close, [data-topic-preview-close], or a <button> whose aria-label / text contains "close".
  • Bootstrap .offcanvas / .modal shells with an id have a visible opener using data-bs-toggle + data-bs-target (or aria-controls) — e.g. the fixed btn btn-forge control targeting #docNavOffcanvas.
  • With the shell open (show / active / aria-hidden="false"), repeated Tab cycles keep document.activeElement inside the shell; main links are not focusable until dismiss.
  • Open role="dialog" overlays expose aria-modal="true" plus aria-labelledby or aria-label (topic preview sets these in ensureTopicPreviewModal()).
  • KS diagram modal uses the shared fragment: #diagramModal + .diagram-modal-close calling closeDiagramModal(); closed state uses the hidden attribute so scaffold text stays out of the tab order.

Failing signals

  • no-dismiss — Shell present (.modal, .offcanvas, or [role="dialog"]) with no dismiss control the probe recognizes.
  • no-trigger — Bootstrap shell has an id but no visible data-bs-target / aria-controls opener in the document (orphan offcanvas/modal).
  • trap-escape — While open, Tab moves focus to an element outside the shell (e.g. a .nav-link in the page chrome).
  • background-tabbable — Focus can land on the first tabbable control inside <main> while the overlay is open.
  • no-focusable — Open shell exposes no tabbable controls (keyboard users cannot reach actions or close).
  • missing-aria-modal — Open dialog overlay has role="dialog" but not aria-modal="true".

Before example

Before (failing example)

Failing KS markup: diagram lightbox open without a close control; background prose remains in the tab order.

After example

After (passing example)

Handbook

Passing KS markup: canonical diagram modal fragment plus handbook offcanvas with Bootstrap dismiss and a paired mobile opener.

Evidence and remediation

Evidence: Metrics field appFocusTrapReport with overlayShellCount, openTestsRun, and violations[] entries tagged no-dismiss, no-trigger, trap-escape, background-tabbable, no-focusable, or missing-aria-modal. Findings cite shell id, truncated className, and the element that received focus when escape was detected.

Remediate (in order):

  1. Add a visible, labeled close.btn-close + data-bs-dismiss="offcanvas" for Bootstrap shells; .diagram-modal-close or [data-topic-preview-close] for KS modals.
  2. Pair Bootstrap shells with data-bs-toggle / data-bs-target on the handbook hamburger (components/layouts.py mobile nav pattern).
  3. When custom JS opens overlays, mirror forgeApplyDiagramModalOpen / closeDiagramModal or topic-preview helpers so hidden / aria-hidden / active stay consistent and background content is inert or aria-hidden.
  4. On open, verify Tab cycles stay inside the shell; if focus escapes, enable Bootstrap's modal/offcanvas focus trap or set inert on main until dismiss.
  5. Set role="dialog" with aria-modal="true" and restore focus to the triggering control on close (DET.NAV.FOCUS_ORDER).
  6. Re-run analyze-website-ux.mjs on handbook and showcase URLs that include diagram expand and mobile nav.