Skip to content

Commit 6e34bbc

Browse files
committed
fix(a11y): prevent header from obscuring skip link target
This commit adds a dynamic `scroll-padding-top` to the `<html>` element based on the header height, so that when users activate the "Skip to the main content" link, the main content is fully visible and not hidden behind the sticky header. I created a custom `useResizeObserver` hook and placed it in the newly-created `src/hooks` directory to update the padding when the header height changes, e.g., when the user resizes the viewport.
1 parent 34e8422 commit 6e34bbc

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

src/components/layout/Navbar.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@ import {
55
SearchIcon,
66
XIcon,
77
} from 'lucide-react';
8-
import React, { useState } from 'react';
8+
import React, { useEffect, useState } from 'react';
99
import { useTranslation } from 'react-i18next';
1010
import { Link } from 'react-router-dom';
1111
import { mainNavigation } from '../../data/navigation';
1212
import { LANGUAGES } from '../../i18n/languages';
1313
import { LanguageType } from '../../types';
14+
import { useResizeObserver } from '@/hooks/use-resize-observer';
1415

1516
const Navbar: React.FC = () => {
1617
const [isOpen, setIsOpen] = useState(false);
1718
const [activeMenu, setActiveMenu] = useState<string | null>(null);
1819
const { t, i18n } = useTranslation('common');
20+
const { ref, height } = useResizeObserver();
1921

2022
const toggleMenu = () => {
2123
setIsOpen(!isOpen);
@@ -37,8 +39,13 @@ const Navbar: React.FC = () => {
3739
i18n.changeLanguage(newLanguage);
3840
};
3941

42+
useEffect(() => {
43+
// Add scroll padding to prevent the sticky header from obscuring the content
44+
document.documentElement.style.scrollPaddingTop = `${height}px`;
45+
}, [height]);
46+
4047
return (
41-
<nav className='bg-white shadow-xs sticky top-0 z-50'>
48+
<nav ref={ref} className='bg-white shadow-xs sticky top-0 z-50'>
4249
{/* Top bar with language switcher and additional links */}
4350
<div className='border-b border-gray-200'>
4451
<div className='container mx-auto px-4 flex justify-end items-center h-10'>

src/hooks/use-resize-observer.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
3+
type Size = {
4+
width: number;
5+
height: number;
6+
};
7+
8+
/**
9+
* useResizeObserver
10+
*
11+
* A hook that observes size changes of a target element.
12+
*
13+
* @param targetRef - React ref pointing to the element you want to observe
14+
* @returns size object {width, height}
15+
*/
16+
export function useResizeObserver<T extends HTMLElement>() {
17+
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
18+
const ref = useRef<T>(null);
19+
20+
useEffect(() => {
21+
const target = ref.current;
22+
if (!target) return;
23+
24+
const observer = new ResizeObserver(entries => {
25+
for (const entry of entries) {
26+
if (entry.contentRect) {
27+
setSize({
28+
width: entry.contentRect.width,
29+
height: entry.contentRect.height,
30+
});
31+
}
32+
}
33+
});
34+
35+
observer.observe(target);
36+
37+
return () => {
38+
observer.disconnect();
39+
};
40+
}, [ref]);
41+
42+
return { ref, width: size.width, height: size.height };
43+
}

0 commit comments

Comments
 (0)