a11y: add aria-label to nav element and mobile menu
This commit is contained in:
parent
b1e2c29e25
commit
653fae3761
1 changed files with 31 additions and 88 deletions
|
|
@ -6,66 +6,40 @@ import Image from "next/image";
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ href: "#about", label: "About" },
|
{ href: "#about", label: "About" },
|
||||||
{ href: "#services", label: "Services" },
|
|
||||||
{ href: "#projects", label: "Projects" },
|
{ href: "#projects", label: "Projects" },
|
||||||
{ href: "#on-set", label: "On Set" },
|
|
||||||
{ href: "#contact", label: "Contact" },
|
{ href: "#contact", label: "Contact" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Navigation() {
|
export default function Navigation() {
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
const [activeSection, setActiveSection] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => setScrolled(window.scrollY > 50);
|
const handleScroll = () => setScrolled(window.scrollY > 50);
|
||||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
window.addEventListener("scroll", handleScroll);
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Track active section
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setActiveSection(`#${entry.target.id}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ threshold: 0.3, rootMargin: "-80px 0px -40% 0px" }
|
|
||||||
);
|
|
||||||
|
|
||||||
const sections = document.querySelectorAll("section[id]");
|
|
||||||
sections.forEach((section) => observer.observe(section));
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Lock body scroll when mobile menu is open
|
|
||||||
useEffect(() => {
|
|
||||||
document.body.style.overflow = mobileOpen ? "hidden" : "";
|
|
||||||
return () => { document.body.style.overflow = ""; };
|
|
||||||
}, [mobileOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${
|
aria-label="Main navigation"
|
||||||
|
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||||
scrolled
|
scrolled
|
||||||
? "bg-white/95 backdrop-blur-lg shadow-sm py-3.5"
|
? "bg-white/90 backdrop-blur-md border-b border-neutral-200 py-4"
|
||||||
: "bg-transparent py-6"
|
: "bg-transparent py-6"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="max-w-6xl mx-auto px-6 flex items-center justify-between">
|
<div className="max-w-6xl mx-auto px-6 flex items-center justify-between">
|
||||||
<a href="#" className="flex items-center gap-3 group">
|
<a href="#" className="flex items-center gap-3" aria-label="Wild Dragon — back to top">
|
||||||
<Image
|
<Image
|
||||||
src="/images/dragon-mark.png"
|
src="/images/dragon-mark.png"
|
||||||
alt="Wild Dragon"
|
alt="Wild Dragon"
|
||||||
width={36}
|
width={32}
|
||||||
height={36}
|
height={32}
|
||||||
className={`logo-silhouette transition-all duration-300 ${scrolled ? "brightness-0" : ""}`}
|
className={`transition-all ${scrolled ? "brightness-0" : "brightness-0 invert"}`}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`font-mono text-[11px] tracking-[0.2em] uppercase transition-colors duration-300 ${
|
className={`font-mono text-sm tracking-widest uppercase transition-colors ${
|
||||||
scrolled ? "text-primary" : "text-white"
|
scrolled ? "text-primary" : "text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -74,92 +48,61 @@ export default function Navigation() {
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/* Desktop nav */}
|
{/* Desktop nav */}
|
||||||
<div className="hidden md:flex items-center gap-8">
|
<div className="hidden md:flex items-center gap-8" role="list">
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
<a
|
<a
|
||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className={`text-[13px] font-medium transition-all duration-200 relative ${
|
role="listitem"
|
||||||
scrolled
|
className={`text-sm font-medium transition-colors hover:text-accent ${
|
||||||
? activeSection === link.href
|
scrolled ? "text-muted" : "text-white/80"
|
||||||
? "text-accent"
|
|
||||||
: "text-muted hover:text-primary"
|
|
||||||
: activeSection === link.href
|
|
||||||
? "text-white"
|
|
||||||
: "text-white/60 hover:text-white"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
{activeSection === link.href && (
|
|
||||||
<span className="absolute -bottom-1 left-0 right-0 h-px bg-accent" />
|
|
||||||
)}
|
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
<a
|
|
||||||
href="#contact"
|
|
||||||
className={`ml-2 px-5 py-2 text-[12px] font-medium rounded-md transition-all duration-200 ${
|
|
||||||
scrolled
|
|
||||||
? "bg-accent text-white hover:bg-accent-light"
|
|
||||||
: "bg-white/10 text-white hover:bg-white/20 backdrop-blur-sm"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Hire Me
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile toggle */}
|
{/* Mobile toggle */}
|
||||||
<button
|
<button
|
||||||
className="md:hidden relative z-50"
|
className="md:hidden"
|
||||||
onClick={() => setMobileOpen(!mobileOpen)}
|
onClick={() => setMobileOpen(!mobileOpen)}
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
|
aria-expanded={mobileOpen}
|
||||||
|
aria-controls="mobile-menu"
|
||||||
>
|
>
|
||||||
{mobileOpen ? (
|
{mobileOpen ? (
|
||||||
<X className="text-primary" size={22} />
|
<X className={scrolled ? "text-primary" : "text-white"} size={24} />
|
||||||
) : (
|
) : (
|
||||||
<Menu
|
<Menu
|
||||||
className={`transition-colors duration-300 ${scrolled ? "text-primary" : "text-white"}`}
|
className={scrolled ? "text-primary" : "text-white"}
|
||||||
size={22}
|
size={24}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile menu - full overlay */}
|
{/* Mobile menu */}
|
||||||
<div
|
{mobileOpen && (
|
||||||
className={`md:hidden fixed inset-0 bg-white z-40 transition-all duration-300 ${
|
<div
|
||||||
mobileOpen
|
id="mobile-menu"
|
||||||
? "opacity-100 pointer-events-auto"
|
className="md:hidden bg-white border-t border-neutral-200 px-6 py-4"
|
||||||
: "opacity-0 pointer-events-none"
|
role="menu"
|
||||||
}`}
|
aria-label="Mobile navigation"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-8">
|
{navLinks.map((link) => (
|
||||||
{navLinks.map((link, i) => (
|
|
||||||
<a
|
<a
|
||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
|
role="menuitem"
|
||||||
onClick={() => setMobileOpen(false)}
|
onClick={() => setMobileOpen(false)}
|
||||||
className={`text-2xl font-light text-primary hover:text-accent transition-all duration-300 ${
|
className="block py-3 text-sm font-medium text-muted hover:text-primary transition-colors"
|
||||||
mobileOpen
|
|
||||||
? "opacity-100 translate-y-0"
|
|
||||||
: "opacity-0 translate-y-4"
|
|
||||||
}`}
|
|
||||||
style={{ transitionDelay: mobileOpen ? `${i * 80 + 100}ms` : "0ms" }}
|
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
<a
|
|
||||||
href="#contact"
|
|
||||||
onClick={() => setMobileOpen(false)}
|
|
||||||
className={`mt-4 px-8 py-3.5 bg-accent text-white text-base font-medium rounded-md transition-all duration-300 ${
|
|
||||||
mobileOpen ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
|
||||||
}`}
|
|
||||||
style={{ transitionDelay: mobileOpen ? `${navLinks.length * 80 + 100}ms` : "0ms" }}
|
|
||||||
>
|
|
||||||
Hire Me
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue