ux: improve navigation accessibility

This commit is contained in:
Marco Allegretti 2026-02-05 12:28:57 +01:00
parent b501c9da75
commit 8ce353262b
3 changed files with 108 additions and 8 deletions

View file

@ -114,6 +114,30 @@
min-height: 100vh; min-height: 100vh;
} }
body.is-scroll-locked {
overflow: hidden;
}
.skip-link {
position: fixed;
top: 0.75rem;
left: 0.75rem;
z-index: 1000;
padding: 0.65rem 0.9rem;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text);
box-shadow: var(--shadow-md);
transform: translateY(calc(-100% - 1rem));
transition: transform var(--motion-normal) var(--easing-standard);
}
.skip-link:focus,
.skip-link:focus-visible {
transform: translateY(0);
}
::selection { ::selection {
background: var(--color-primary-muted); background: var(--color-primary-muted);
} }

View file

@ -81,6 +81,7 @@ const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_S
</script> </script>
</head> </head>
<body> <body>
<a class="skip-link" href="#main-content">Skip to content</a>
<VotingIcons /> <VotingIcons />
<div class="app"> <div class="app">
<header class="header"> <header class="header">
@ -109,7 +110,7 @@ const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_S
</div> </div>
</nav> </nav>
</header> </header>
<main class="main"> <main class="main" id="main-content" tabindex="-1">
<slot /> <slot />
</main> </main>
<footer class="footer"> <footer class="footer">
@ -121,15 +122,29 @@ const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_S
const nav = document.getElementById('site-nav'); const nav = document.getElementById('site-nav');
const navToggle = document.getElementById('nav-toggle'); const navToggle = document.getElementById('nav-toggle');
function setNavOpen(open) { function openNav() {
if (!nav || !navToggle) return; if (!nav || !navToggle) return;
nav.classList.toggle('is-open', open); nav.classList.add('is-open');
navToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); navToggle.setAttribute('aria-expanded', 'true');
if (window.matchMedia('(max-width: 640px)').matches) {
document.body.classList.add('is-scroll-locked');
}
}
function closeNav() {
if (!nav || !navToggle) return;
nav.classList.remove('is-open');
navToggle.setAttribute('aria-expanded', 'false');
document.body.classList.remove('is-scroll-locked');
} }
if (nav && navToggle) { if (nav && navToggle) {
navToggle.addEventListener('click', () => { navToggle.addEventListener('click', () => {
setNavOpen(!nav.classList.contains('is-open')); if (nav.classList.contains('is-open')) {
closeNav();
} else {
openNav();
}
}); });
nav.addEventListener('click', (event) => { nav.addEventListener('click', (event) => {
@ -138,13 +153,27 @@ const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_S
if (target.closest('#nav-toggle')) return; if (target.closest('#nav-toggle')) return;
if (!target.closest('a')) return; if (!target.closest('a')) return;
if (window.matchMedia('(max-width: 640px)').matches) { if (window.matchMedia('(max-width: 640px)').matches) {
setNavOpen(false); closeNav();
}
});
document.addEventListener('click', (event) => {
if (!nav.classList.contains('is-open')) return;
const target = event.target;
if (!(target instanceof Node)) return;
if (nav.contains(target)) return;
closeNav();
});
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeNav();
} }
}); });
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
if (window.innerWidth > 640) { if (window.innerWidth > 640) {
setNavOpen(false); closeNav();
} }
}); });
} }

View file

@ -80,6 +80,7 @@ const defaultTheme = DEFAULT_THEME;
</script> </script>
</head> </head>
<body> <body>
<a class="skip-link" href="#main-content">Skip to content</a>
<div class="public-app"> <div class="public-app">
<header class="public-header"> <header class="public-header">
<nav class="public-nav" id="public-nav"> <nav class="public-nav" id="public-nav">
@ -108,7 +109,7 @@ const defaultTheme = DEFAULT_THEME;
</div> </div>
</nav> </nav>
</header> </header>
<main class="public-main"> <main class="public-main" id="main-content" tabindex="-1">
<slot /> <slot />
</main> </main>
<footer class="public-footer"> <footer class="public-footer">
@ -156,15 +157,39 @@ const defaultTheme = DEFAULT_THEME;
if (!nav || !toggle) return; if (!nav || !toggle) return;
nav.classList.add('is-open'); nav.classList.add('is-open');
toggle.setAttribute('aria-expanded', 'true'); toggle.setAttribute('aria-expanded', 'true');
if (window.matchMedia('(max-width: 768px)').matches) {
document.body.classList.add('is-scroll-locked');
}
} }
function closeNav() { function closeNav() {
if (!nav || !toggle) return; if (!nav || !toggle) return;
nav.classList.remove('is-open'); nav.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false'); toggle.setAttribute('aria-expanded', 'false');
document.body.classList.remove('is-scroll-locked');
}
function setActiveNav() {
const links = document.querySelectorAll('#public-nav-menu a');
const path = window.location.pathname.replace(/\/$/, '') || '/';
links.forEach((a) => {
if (!(a instanceof HTMLAnchorElement)) return;
const hrefPath = a.pathname.replace(/\/$/, '') || '/';
if (hrefPath === '/' || hrefPath === '') {
return;
}
const isActive = path === hrefPath || path.indexOf(hrefPath + '/') === 0;
if (isActive) {
a.setAttribute('aria-current', 'page');
}
});
} }
if (nav && toggle) { if (nav && toggle) {
setActiveNav();
toggle.addEventListener('click', () => { toggle.addEventListener('click', () => {
if (nav.classList.contains('is-open')) { if (nav.classList.contains('is-open')) {
closeNav(); closeNav();
@ -181,6 +206,20 @@ const defaultTheme = DEFAULT_THEME;
}); });
}); });
document.addEventListener('click', (event) => {
if (!nav.classList.contains('is-open')) return;
const target = event.target;
if (!(target instanceof Node)) return;
if (nav.contains(target)) return;
closeNav();
});
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeNav();
}
});
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
if (window.innerWidth > 768) { if (window.innerWidth > 768) {
closeNav(); closeNav();
@ -331,6 +370,14 @@ const defaultTheme = DEFAULT_THEME;
width: 100%; width: 100%;
} }
.nav-links a[aria-current='page'] {
color: var(--color-text);
}
.nav-links a[aria-current='page']::after {
width: 100%;
}
.nav-actions { .nav-actions {
display: flex; display: flex;
gap: 1rem; gap: 1rem;