Improve accessibility: keyboard-operable mobile nav, skip link, focus styles

- Replace the checkbox-hack mobile menu (display:none checkbox was
  unreachable by keyboard/screen readers) with a real button using
  aria-expanded/aria-controls
- Add a skip-to-content link
- Add a consistent :focus-visible outline across interactive elements
- Underline prose links by default instead of only on hover (WCAG 1.4.1
  use-of-color), keep nav/button links unstyled since they're already
  visually distinct
- Respect prefers-reduced-motion for smooth scrolling

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Pascal Scholz 2026-09-24 23:10:23 +02:00
parent cf8d6a65ea
commit 807022a62a
4 changed files with 97 additions and 16 deletions

View file

@ -51,6 +51,7 @@ const footer = site.footer;
.footer-nav a { .footer-nav a {
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
text-decoration: none;
} }
.footer-nav a:hover { .footer-nav a:hover {

View file

@ -10,14 +10,20 @@ const nav = site.nav;
<img src={nav.logo} alt={nav.logoAlt} width="320" height="60" class="nav-logo-img" /> <img src={nav.logo} alt={nav.logoAlt} width="320" height="60" class="nav-logo-img" />
</a> </a>
<input type="checkbox" id="nav-toggle" class="nav-toggle-checkbox" /> <button
<label for="nav-toggle" class="nav-toggle" aria-label="Menü öffnen/schließen"> type="button"
class="nav-toggle"
id="nav-toggle"
aria-expanded="false"
aria-controls="nav-links"
aria-label="Menü öffnen"
>
<span></span> <span></span>
<span></span> <span></span>
<span></span> <span></span>
</label> </button>
<nav class="nav-links" aria-label="Hauptnavigation"> <nav class="nav-links" id="nav-links" aria-label="Hauptnavigation">
<ul> <ul>
{nav.links.map((item) => ( {nav.links.map((item) => (
<li> <li>
@ -50,9 +56,6 @@ const nav = site.nav;
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
}
.nav-logo:hover {
text-decoration: none; text-decoration: none;
} }
@ -78,11 +81,11 @@ const nav = site.nav;
color: var(--color-text-muted); color: var(--color-text-muted);
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
text-decoration: none;
} }
.nav-links ul a:hover { .nav-links ul a:hover {
color: var(--color-primary); color: var(--color-primary);
text-decoration: none;
} }
.nav-cta { .nav-cta {
@ -92,10 +95,6 @@ const nav = site.nav;
padding: var(--space-3) var(--space-5); padding: var(--space-3) var(--space-5);
} }
.nav-toggle-checkbox {
display: none;
}
.nav-toggle { .nav-toggle {
display: none; display: none;
flex-direction: column; flex-direction: column;
@ -103,6 +102,9 @@ const nav = site.nav;
gap: 5px; gap: 5px;
width: 28px; width: 28px;
height: 22px; height: 22px;
padding: 0;
background: none;
border: none;
cursor: pointer; cursor: pointer;
} }
@ -149,8 +151,34 @@ const nav = site.nav;
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
} }
.nav-toggle-checkbox:checked ~ .nav-links { .nav-links.is-open {
display: flex; display: flex;
} }
} }
</style> </style>
<script>
const toggle = document.getElementById('nav-toggle');
const links = document.getElementById('nav-links');
function setOpen(open) {
links?.classList.toggle('is-open', open);
toggle?.setAttribute('aria-expanded', String(open));
toggle?.setAttribute('aria-label', open ? 'Menü schließen' : 'Menü öffnen');
}
toggle?.addEventListener('click', () => {
setOpen(!links?.classList.contains('is-open'));
});
links?.addEventListener('click', (event) => {
const target = event.target;
if (target instanceof HTMLElement && target.tagName === 'A') {
setOpen(false);
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') setOpen(false);
});
</script>

View file

@ -32,8 +32,9 @@ const fontsHref = googleFontsHref(theme);
<style set:html={themeCss(theme)} /> <style set:html={themeCss(theme)} />
</head> </head>
<body> <body>
<a href="#main-content" class="skip-link">Zum Inhalt springen</a>
<Nav /> <Nav />
<main> <main id="main-content">
<slot /> <slot />
</main> </main>
<Footer /> <Footer />

View file

@ -123,11 +123,23 @@ p {
a { a {
color: var(--color-primary); color: var(--color-primary);
text-decoration: none; text-decoration: underline;
text-underline-offset: 0.15em;
} }
a:hover { a:hover {
text-decoration: underline; color: var(--color-primary-dark);
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: var(--radius-sm);
} }
ul { ul {
@ -147,6 +159,25 @@ textarea {
font-size: var(--font-size-base); font-size: var(--font-size-base);
} }
.skip-link {
position: absolute;
top: 0;
left: var(--space-4);
transform: translateY(-150%);
z-index: 100;
background: var(--color-primary);
color: var(--color-primary-contrast);
padding: var(--space-3) var(--space-5);
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
font-weight: var(--font-weight-semibold);
text-decoration: none;
transition: transform var(--transition-fast);
}
.skip-link:focus {
transform: translateY(0);
}
/* ========================================================================== /* ==========================================================================
Layout-Hilfsklassen Layout-Hilfsklassen
========================================================================== */ ========================================================================== */
@ -249,3 +280,23 @@ textarea {
border-bottom: 1px dashed var(--color-flag); border-bottom: 1px dashed var(--color-flag);
font-style: normal; font-style: normal;
} }
/* ==========================================================================
Bewegungsreduktion
Respektiert die Systemeinstellung fuer Nutzer:innen mit vestibulaeren
Beeintraechtigungen (WCAG 2.3.3).
========================================================================== */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}