diff --git a/CHANGELOG.md b/CHANGELOG.md
index 233091e1..a9c38d00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ and this project adheres to
- ♿️(frontend) upgrade join meeting modal accessibility #1027
- ⬆️(python) bump minimal required python version to 3.13 #1033
- ♿️(frontend) improve accessibility of the IntroSlider carousel #1026
+- ♿️(frontend) add skip link component for keyboard navigation #1019
### Fixed
diff --git a/src/frontend/src/layout/Layout.tsx b/src/frontend/src/layout/Layout.tsx
index ce2235d8..38e657ea 100644
--- a/src/frontend/src/layout/Layout.tsx
+++ b/src/frontend/src/layout/Layout.tsx
@@ -5,6 +5,7 @@ import { layoutStore } from '@/stores/layout'
import { useSnapshot } from 'valtio'
import { Footer } from '@/layout/Footer'
import { ScreenReaderAnnouncer } from '@/primitives'
+import { SkipLink, MAIN_CONTENT_ID } from './SkipLink'
export type Layout = 'fullpage' | 'centered'
@@ -21,6 +22,7 @@ export const Layout = ({ children }: { children: ReactNode }) => {
return (
<>
+ {showHeader && }
{
>
{showHeader && }
{
+ const { t } = useTranslation()
+
+ const handleClick = (e: MouseEvent) => {
+ e.preventDefault()
+ const main = document.getElementById(MAIN_CONTENT_ID)
+ if (!main) return
+
+ const heading = main.querySelector('h1, h2, h3') as HTMLElement | null
+ const target = heading ?? main
+
+ if (!target.hasAttribute('tabindex')) {
+ target.setAttribute('tabindex', '-1')
+ }
+ target.focus()
+ }
+
+ return (
+
+ {t('skipLink')}
+
+ )
+}
diff --git a/src/frontend/src/locales/de/global.json b/src/frontend/src/locales/de/global.json
index ac8e3e02..3baa362d 100644
--- a/src/frontend/src/locales/de/global.json
+++ b/src/frontend/src/locales/de/global.json
@@ -52,6 +52,7 @@
"label": "OK"
}
},
+ "skipLink": "Zum Hauptinhalt springen",
"clipboardContent": {
"url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}",
"numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}"
diff --git a/src/frontend/src/locales/en/global.json b/src/frontend/src/locales/en/global.json
index c9adbd13..229e0d16 100644
--- a/src/frontend/src/locales/en/global.json
+++ b/src/frontend/src/locales/en/global.json
@@ -52,6 +52,7 @@
"label": "OK"
}
},
+ "skipLink": "Skip to main content",
"clipboardContent": {
"url": "To join the video conference, click on this link: {{roomUrl}}",
"numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}"
diff --git a/src/frontend/src/locales/fr/global.json b/src/frontend/src/locales/fr/global.json
index 051e95ee..8eb6ccf4 100644
--- a/src/frontend/src/locales/fr/global.json
+++ b/src/frontend/src/locales/fr/global.json
@@ -52,6 +52,7 @@
"label": "OK"
}
},
+ "skipLink": "Aller au contenu principal",
"clipboardContent": {
"url": "Pour participer à la visioconférence, cliquez sur ce lien : {{roomUrl}}",
"numberAndPin": "Pour participer par téléphone, composez le {{phoneNumber}} et saisissez ce code : {{pinCode}}"
diff --git a/src/frontend/src/locales/nl/global.json b/src/frontend/src/locales/nl/global.json
index 292d97a3..4cb66436 100644
--- a/src/frontend/src/locales/nl/global.json
+++ b/src/frontend/src/locales/nl/global.json
@@ -51,6 +51,7 @@
"label": "OK"
}
},
+ "skipLink": "Naar de hoofdinhoud gaan",
"clipboardContent": {
"url": "Klik op deze link om deel te nemen aan de videoconferentie: {{roomUrl}}",
"numberAndPin": "Bel {{phoneNumber}} en voer deze code in om telefonisch deel te nemen: {{pinCode}}"
diff --git a/src/frontend/src/styles/index.css b/src/frontend/src/styles/index.css
index 2aabeb8d..900d7a3a 100644
--- a/src/frontend/src/styles/index.css
+++ b/src/frontend/src/styles/index.css
@@ -22,6 +22,11 @@ body,
outline: 2px solid transparent;
}
+main#main-content :is(h1, h2, h3)[tabindex='-1']:focus {
+ outline: 2px solid var(--colors-focus-ring);
+ outline-offset: 2px;
+}
+
[data-rac][data-focus-visible]:not(label, .react-aria-Select),
:is(a, button, input[type='text'], select, textarea):not(
[data-rac]