Evidence — footer in-page toggles are <button>, not <a href="#"> (issue #320)¶
Three footer controls opened in-page UI — the #info-section offcanvas (emitted twice, once in each branch of the powered-by loop) and the #cookieSettingsModal modal — but shipped as <a href="#" data-bs-toggle>. Assistive tech announced them as links (WCAG 4.1.2 Name, Role, Value), and whenever Bootstrap's toggle did not fire the browser followed href="#" and jumped the page to the top. All three are now <button type="button">; the back-to-top anchor keeps its href="#", because scrolling to the top is its actual behaviour rather than a side effect.
The point of the change is that it is visually inert: the footer must look exactly as it did while announcing the right role. That makes the interesting comparison different from the navbar fix's. A markup swap cannot be reverted with CSS, so there is no unfixCss band montage here — what CSS can revert is the other half of the fix, the <button> reset in _sass/components/_footer.scss, and that is what 02-configs.png contrasts.
Generated by test/visual/footer-button-semantics-evidence.mjs.
01-viewport-matrix.png— the whole footer cropped at 375 / 768 / 1280px as shipped. "Info" and "Cookie Preferences" render as the plain text controls the anchors were: no button chrome, no border, no box, no baseline shift, and no clipping or page overflow at any width.02-configs.png— the load-bearing image, at 1280px:- shipped —
<button>plus the_footer.scssreset. Identical to the pre-fix anchors. - naive — the same markup with the reset stripped (
appearance,background,border,color,paddingreverted). This is what a markup-only swap would have shipped: UA button chrome, visible as a box around "ⓘ Info" in the powered-by row and around "Cookie Preferences" in the policy row. It is the reason.powered-by-linkwas made element-agnostic and.footer-inline-buttonwas added, rather than reaching forbtn btn-link p-0(.btnre-adds its own padding,line-height, and a 1px transparent border — a 2px box shift and a likely snapshot diff). metrics.json— footer overflow per width.0pxat 375 / 768 / 1280 in both configurations, so the reset removes the chrome without introducing overflow.
How this run was produced¶
Against the production _config.yml build rather than the docker compose dev server named in the kit's usage block: the environment that generated this had no bundler and no permission to run docker, so the site was taken from the github-pages artifact that pages.yml builds for this very pull request (run 32951285286) and served statically on :4000. The two configs differ only in analytics and remote_theme — neither renders in the footer — and the served markup was confirmed to carry <button type="button" class="powered-by-link"> and <button type="button" class="footer-inline-button"> before any screenshot was taken.
To regenerate the conventional way:
docker compose up # serves :4000
BASE_URL=http://localhost:4000 node test/visual/footer-button-semantics-evidence.mjs
Why the regression test cannot use axe¶
AxeBuilder cannot detect this defect. An <a> that has an href is a well-formed link to axe-core, and no rule in wcag2a/wcag2aa — or outside it — fires on "anchor used as a button". An axe-based assertion passes on the broken markup too. The regression test in test/visual/core/accessibility.spec.js therefore asserts element identity directly: no footer a[href="#"][data-bs-toggle] survives, both footer button[data-bs-target="#info-section"] and footer button[data-bs-target="#cookieSettingsModal"] exist, and each still opens its dialog when clicked.
Two coverage regressions the swap would otherwise have caused are fixed in the same change: _sass/components/_footer.scss's ul.list-inline li > a tap-target rule now also matches > button (Cookie Preferences would have dropped below the WCAG 2.5.8 24px minimum), and test/visual/core/responsive.spec.js now walks a, button under .footer-dark-block instead of a alone.