Skip to content

a11y: no visible skip link — #main-content landmark exists but 19 nav items precede content in tab order #17565

Closed

Description

@mrveiss

Part of #12730.

Problem

autobot-frontend has the landmark but not the affordance. src/App.vue:536 renders <main id="main-content" …>, and src/App.vue:41 renders the main navigation — but there is no visible skip link, so a keyboard or screen-reader user tabs through the whole navigation on every page before reaching content. The phrase "Skip to main content" appears in 0 files; skip-link appears in 1.

The target already exists, so this is a missing ~10-line affordance rather than a structural change.

Approach

  1. Add a skip link as the first focusable element in App.vue, pointing at #main-content.
  2. Visually hidden until focused, then visible with the standard focus ring — the codebase already styles focus-visible in 102 files, so match that.
  3. Translate the label in all 11 locales.
  4. Confirm the target receives focus: #main-content already carries the landmark, and a tabindex="-1" on it is what makes the jump actually move focus rather than only the scroll position.

Acceptance criteria

  • A skip link is the first focusable element on every authenticated page.
  • It is invisible until focused and clearly visible when focused.
  • Activating it moves focus (not just scroll) to #main-content — asserted in a test.
  • The label exists in all 11 locales.

Implementation Order

Wave: 1
Depends on: —
Unblocks: —

Evidence

src/App.vue:536 (<main id="main-content">) · src/App.vue:41 (main navigation, 19 nav destinations ahead of content in tab order) · 0 files containing "Skip to main content".

Found during a comparative GUI-standardisation audit — see docs/research/dashboard-shell-and-component-standardisation.md, gap G12.


Correction (2026-09-26): the component already exists — in the other GUI

This issue was filed after auditing autobot-frontend only. autobot-slm-frontend already has one:

autobot-slm-frontend/src/components/common/SkipLink.vue          (label: "Skip to main content")
autobot-slm-frontend/src/components/common/SkipLink.stories.ts

So this is not "build a skip link", it is promote the existing one into libs/autobot-ui and adopt it in autobot-frontend — the same wire-in-don't-duplicate pattern as #14776/#14778. Building a second implementation would create exactly the kind of fork this umbrella exists to remove.

Revised approach:

  1. Promote SkipLink.vue + its story from autobot-slm-frontend into libs/autobot-ui, rendering off the token contract alone.
  2. Leave a re-export at the old SLM path so its call site is untouched.
  3. Adopt in autobot-frontend/src/App.vue, targeting the existing #main-content at line 536.
  4. Keep the tabindex="-1" requirement from the original acceptance criteria — it is what makes the jump move focus rather than only scroll.

Revised acceptance criteria (replacing the first bullet above):

  • libs/autobot-ui exports SkipLink; autobot-slm-frontend consumes it via re-export, not its own copy.
  • autobot-frontend/src/App.vue renders it as the first focusable element.
  • Label translated in all 11 locales (SLM is not yet on all 11 — see Bring autobot-slm-frontend to all 11 locales #14781 — so the key must exist in the kit's locale surface, not only SLM's).

Depends on #14776 (the fork-retirement pattern this follows).

Activity

  1. mrveiss commented on Oct 4, 2026

    @mrveiss
    OwnerAuthor

    Premise correction, checked against current origin/main before implementing.

    autobot-frontend has not been without a skip link. Since 2026-02, src/App.vue (first child of #app, inside v-if="showAuthChrome") has rendered two hand-rolled skip links, #main-content and #navigation, styled visible-on-focus by a local .skip-link block.

    What was actually missing:

    • Labels never translated. nav.skipToContent / nav.skipToNavigation held the English "Skip to content" / "Skip to navigation" in all 11 locales.
    • Focus didn't move. <main id="main-content"> has no tabindex, so activating the link scrolled but left focus where it was.
    • Two implementations. The SLM console's components/common/SkipLink.vue, with a hard-coded English default label, coexisted with the hand-rolled one.

    So the work is consolidation plus i18n plus the focus fix, not a new affordance. It is being delivered by the batch-3 frontend PR (kit SkipLink in libs/autobot-ui, adopted by both consoles).

  2. mrveiss commented on Oct 4, 2026

    @mrveiss
    OwnerAuthor

    Delivered by #17971 (merge 90729b5999). Verified against origin/main. As corrected on this issue, the work was consolidating onto one kit SkipLink, not adding a first one.

    • A skip link is the first focusable element on every authenticated page.
      • App.vue:3-7: the first children of #app, inside v-if="showAuthChrome".
      • They render SkipLink from @autobot/ui (libs/autobot-ui/index.ts:33). The SLM's components/common/SkipLink.vue is now a thin wrapper over it.
    • It is invisible until focused and clearly visible when focused.
      • .aui-skip-link: translateY(-200%) until :focus, then translateY(0) with the token focus ring.
    • Activating it moves focus (not just scroll) to #main-content — asserted in a test.
      • libs/autobot-ui/src/components/__tests__/SkipLink.test.ts, case moves focus to a non-focusable landmark when activated (document.activeElement is the <main>, tabindex="-1").
    • The label exists in all 11 locales.
      • nav.skipToContent / nav.skipToNavigation are now translated in all 11; they were English in every locale before.
      • The SLM's common.skipLink.mainContent is lifted from them by the lifting rule.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions