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 {
color: var(--color-text-muted);
font-size: var(--font-size-sm);
text-decoration: none;
}
.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" />
</a>
<input type="checkbox" id="nav-toggle" class="nav-toggle-checkbox" />
<label for="nav-toggle" class="nav-toggle" aria-label="Menü öffnen/schließen">
<button
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>
</label>
</button>
<nav class="nav-links" aria-label="Hauptnavigation">
<nav class="nav-links" id="nav-links" aria-label="Hauptnavigation">
<ul>
{nav.links.map((item) => (
<li>
@ -50,9 +56,6 @@ const nav = site.nav;
display: flex;
align-items: center;
gap: var(--space-3);
}
.nav-logo:hover {
text-decoration: none;
}
@ -78,11 +81,11 @@ const nav = site.nav;
color: var(--color-text-muted);
font-weight: var(--font-weight-medium);
font-size: var(--font-size-sm);
text-decoration: none;
}
.nav-links ul a:hover {
color: var(--color-primary);
text-decoration: none;
}
.nav-cta {
@ -92,10 +95,6 @@ const nav = site.nav;
padding: var(--space-3) var(--space-5);
}
.nav-toggle-checkbox {
display: none;
}
.nav-toggle {
display: none;
flex-direction: column;
@ -103,6 +102,9 @@ const nav = site.nav;
gap: 5px;
width: 28px;
height: 22px;
padding: 0;
background: none;
border: none;
cursor: pointer;
}
@ -149,8 +151,34 @@ const nav = site.nav;
border-bottom: 1px solid var(--color-border);
}
.nav-toggle-checkbox:checked ~ .nav-links {
.nav-links.is-open {
display: flex;
}
}
</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)} />
</head>
<body>
<a href="#main-content" class="skip-link">Zum Inhalt springen</a>
<Nav />
<main>
<main id="main-content">
<slot />
</main>
<Footer />

View file

@ -123,11 +123,23 @@ p {
a {
color: var(--color-primary);
text-decoration: none;
text-decoration: underline;
text-underline-offset: 0.15em;
}
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 {
@ -147,6 +159,25 @@ textarea {
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
========================================================================== */
@ -249,3 +280,23 @@ textarea {
border-bottom: 1px dashed var(--color-flag);
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;
}
}