Skip to content

feat(a11y): add skip link and main landmarks - #315

Open
nva138 wants to merge 2 commits into
Improwised:mainfrom
nva138:a11y/skip-link
Open

nva138 wants to merge 2 commits into
Improwised:mainfrom
nva138:a11y/skip-link

Conversation

@nva138

@nva138 nva138 commented Sep 4, 2026

Copy link
Copy Markdown

Fixes Issue

Addresses #309, specifically the skip-to-content link part. I left it as "Addresses" and not "Closes" since the issue lists a few other a11y tasks that aren't part of this PR.

Changes proposed

I picked the skip link since it's a small, self-contained one to start with.

Right now you have to tab through the whole nav before you get to the content, so I added a skip link as the first focusable element in app.vue. It's hidden by default with sr-only and only shows up once you tab to it.

For it to actually jump somewhere I gave the layouts a main landmark to target:

  • auth.vue already had a , so I just added id="main-content" and tabindex="-1"
  • empty.vue didn't have one, so I turned the content wrapper into and left the sidebar outside of it

Checked it with the keyboard, Tab shows the link and Enter jumps focus into the main area.

[x] - Correct; marked as done
[X] - Correct; marked as done

[ ] - Not correct; marked as not done
-->

Check List (Check all the applicable boxes)

  • [ X] My code follows the code style of this project.
  • My change requires changes to the documentation.
  • I have updated the documentation accordingly.
  • [X ] All new and existing tests passed.
  • [ X] This PR does not contain plagiarized content.
  • [X ] The title of my pull request is a short description of the requested changes.

Screenshots

Screenshot 2026-09-04 at 22 28 28

Note to reviewers

Comment thread app/layouts/empty.vue Outdated
<div class="lg:flex lg:h-screen lg:overflow-hidden">
<Sidebar v-if="!hideSidebar" />
<div class="lg:flex-1 lg:overflow-y-auto">
<main id="main-content" tabindex="-1" class="lg:flex-1 lg:overflow-y-auto">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested main landmarks — several pages wrapped by this layout already declare their own <main>:

  • app/pages/index.vue:2 wraps HeroSection/JoinLiveQuiz/PublicQuizSection in <main>
  • app/pages/admin/index.vue:2 does the same

Converting this <div> to <main> means the DOM ends up with <main><main>…</main></main>. Nested/duplicate main landmarks are an ARIA issue: screen readers announce a "main" region inside the main region, and the real content landmark becomes ambiguous.

Suggestion: keep one main landmark per page. Either:

  • keep the wrapper here but change it back to a <div> and rely on the pages' own <main> (then point the skip link at the page-level main), or
  • keep <main> here and remove/demote the inner <main> in the affected pages (e.g. index.vue, admin/index.vue).

Comment thread app/app.vue
@@ -1,4 +1,9 @@
<template>
<a
href="#main-content"

@ehsaaschaudhary-itpl ehsaaschaudhary-itpl Sep 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate id="main-content" + link-target depth

  1. app/components/FinalScoreBoard.vue already hardcodes id="main-content" (lines ~320 and ~526, both role="main"). On scoreboard pages the DOM will contain 3 elements with the same ID — invalid HTML. The browser anchors to the first match in DOM order, and screen-reader users get multiple identical regions. Worth de-duplicating (unique IDs) so the #main-content target is unambiguous.

  2. The target resolves to the id="main-content" defined in the layouts, i.e. the page-content wrapper. Skips are usually most useful when they land on the actual content landmark / first h1 of the page (past any page-level header/action rows, e.g. LandingTopActions). Consider pointing the anchor at the real content heading on each page, or documenting that the layout landmark is the intended stop.

Nice work on the sr-only focus - works quite well for the feature itself

@ehsaaschaudhary-itpl ehsaaschaudhary-itpl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary — thanks for this a11y contribution! The skip link + target pattern is correct and well-tested with the keyboard.

Left two inline suggestions:

  1. Nested <main> landmarks (on empty.vue): pages like index.vue and admin/index.vue already contain their own <main>, so the layout change creates <main><main> nesting. Pick one landmark layer per page.

  2. Duplicate id="main-content" + target depth (on app.vue): FinalScoreBoard.vue already uses id="main-content" (twice), so on scoreboard pages there will be 3 duplicate IDs; the anchor will jump to the first match in DOM order. De-duplicate the IDs and consider linking closer to the real content heading.

cc @nva138 — happy to help iterate on the nested-landmark cleanup if useful.

@nva138

nva138 commented Sep 20, 2026

Copy link
Copy Markdown
Author

Sorry for the delay on this one.

Both fixed. Layout is a plain div again, id="main-content" and tabindex="-1" stay there so the skip link still has a target everywhere. It's not just index.vue and admin/index.vue by the way, 15 places declare their own <main> (all admin pages, plus QuestionSpace, WaitingSpace, ScoreSpace, FinalScoreBoard and the skeletons), so the layout really shouldn't be one. Left auth.vue as <main>, the pages under it don't have their own.

Also dropped id="main-content" from both mains in FinalScoreBoard.vue. Small thing: it's 2 duplicates rather than 3, those two are in <ClientOnly v-if> / <ClientOnly v-else> so only one renders.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants