Skip to main content
Glama
overlay-navigation.md8.45 kB
# Overlay Navigation Pattern ## Overview Full-screen or expandable overlay navigation that provides immersive menu experience with smooth animations. ## Characteristics - Full-screen or large overlay that appears on menu activation - Animated transitions using Framer Motion - Desktop shows minimal header, mobile shows hamburger - Context-aware hover states - Dismisses on navigation or escape key - **Common in**: Agency sites, creative portfolios, brand-focused experiences ## Implementation Pattern A: Disclosure-Based Overlay ### Structure ```tsx 'use client' import { Disclosure } from '@headlessui/react' import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline' import { motion, AnimatePresence } from 'framer-motion' import Link from 'next/link' export function OverlayNav() { return ( <Disclosure as="nav"> {({ open }) => ( <> {/* Header Bar */} <div className="fixed inset-x-0 top-0 z-50 flex items-center justify-between p-6"> <Link href="/" className="flex items-center"> <Logo className="h-8 w-auto" /> </Link> <Disclosure.Button className="rounded-full bg-white/90 p-2 shadow-lg ring-1 ring-black/10"> {open ? ( <XMarkIcon className="h-6 w-6" /> ) : ( <Bars3Icon className="h-6 w-6" /> )} </Disclosure.Button> </div> {/* Overlay Panel */} <AnimatePresence> {open && ( <Disclosure.Panel as={motion.div} static initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-40 flex items-center justify-center bg-gray-900" > <nav className="flex flex-col items-center space-y-8"> {navItems.map((item, index) => ( <motion.div key={item.name} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.1 }} > <Link href={item.href} className="text-4xl font-bold text-white hover:text-gray-300" > {item.name} </Link> </motion.div> ))} </nav> </Disclosure.Panel> )} </AnimatePresence> </> )} </Disclosure> ) } ``` ## Implementation Pattern B: Context-Based Overlay ### With Motion Config and Reduced Motion ```tsx 'use client' import { createContext, useContext, useState, useId } from 'react' import { motion, MotionConfig, useReducedMotion } from 'framer-motion' import Link from 'next/link' const NavigationContext = createContext<{ open: boolean setOpen: (open: boolean) => void } | null>(null) export function RootLayout({ children }: { children: React.ReactNode }) { const [open, setOpen] = useState(false) const shouldReduceMotion = useReducedMotion() return ( <NavigationContext.Provider value={{ open, setOpen }}> <MotionConfig transition={shouldReduceMotion ? { duration: 0 } : undefined}> <div className="relative flex min-h-full flex-col"> <Header /> <motion.div layout style={{ borderRadius: 40 }} className="relative flex-auto" > {children} </motion.div> {open && <Navigation />} </div> </MotionConfig> </NavigationContext.Provider> ) } function Header() { const { open, setOpen } = useContext(NavigationContext)! return ( <header className="fixed inset-x-0 top-0 z-50 flex items-center justify-between p-6"> <Logo /> <button onClick={() => setOpen(!open)} className="rounded-full bg-white p-2 shadow-lg" > {open ? <XMarkIcon /> : <Bars3Icon />} </button> </header> ) } function Navigation() { const { setOpen } = useContext(NavigationContext)! return ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-40 flex items-center justify-center bg-gray-900/95 backdrop-blur-sm" onClick={() => setOpen(false)} > <nav onClick={(e) => e.stopPropagation()}> {/* Navigation content */} </nav> </motion.div> ) } ``` ## Animation Patterns ### Staggered Link Animations ```tsx {navItems.map((item, index) => ( <motion.div key={item.name} initial={{ opacity: 0, rotateY: -90 }} animate={{ opacity: 1, rotateY: 0 }} transition={{ delay: index * 0.1, duration: 0.5, type: 'spring' }} > <Link href={item.href}> {item.name} </Link> </motion.div> ))} ``` ### Scale and Fade ```tsx <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} transition={{ duration: 0.2 }} > {/* Overlay content */} </motion.div> ``` ### Slide from Edge ```tsx <motion.div initial={{ x: '100%' }} animate={{ x: 0 }} exit={{ x: '100%' }} transition={{ type: 'spring', damping: 25, stiffness: 200 }} > {/* Slide-in panel */} </motion.div> ``` ## Styling Variations ### Dark Overlay with Blur ```tsx className="fixed inset-0 bg-gray-900/95 backdrop-blur-sm" ``` ### Gradient Overlay ```tsx className="fixed inset-0 bg-gradient-to-br from-indigo-600 to-purple-600" ``` ### Light Overlay ```tsx className="fixed inset-0 bg-white" ``` ## Navigation Link Styling ### Large Display Links ```tsx <Link href={item.href} className="text-5xl font-bold tracking-tight text-white transition hover:text-gray-300 md:text-6xl lg:text-7xl" > {item.name} </Link> ``` ### With Underline Animation ```tsx <Link href={item.href} className="group relative text-4xl font-bold text-white" > {item.name} <span className="absolute inset-x-0 -bottom-2 h-1 origin-left scale-x-0 bg-white transition-transform group-hover:scale-x-100" /> </Link> ``` ### With Icons ```tsx <Link href={item.href} className="flex items-center space-x-4 text-3xl font-bold text-white" > <item.icon className="h-8 w-8" /> <span>{item.name}</span> </Link> ``` ## Accessibility Features ### Keyboard Navigation ```tsx // Escape key to close useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) } if (open) { document.addEventListener('keydown', handleEscape) return () => document.removeEventListener('keydown', handleEscape) } }, [open, setOpen]) ``` ### Focus Management ```tsx // Focus first link when opened const firstLinkRef = useRef<HTMLAnchorElement>(null) useEffect(() => { if (open && firstLinkRef.current) { firstLinkRef.current.focus() } }, [open]) ``` ### ARIA Attributes ```tsx <button aria-label="Toggle navigation" aria-expanded={open} onClick={() => setOpen(!open)} > {open ? <XMarkIcon /> : <Bars3Icon />} </button> ``` ## Mobile Considerations ### Touch-Friendly Targets - Minimum tap target: 44×44px - Adequate spacing between links - Avoid hover-only interactions ### Performance - Use `will-change` sparingly - Respect `prefers-reduced-motion` - Optimize for 60fps animations ## Advanced Features ### Close on Route Change ```tsx import { usePathname } from 'next/navigation' import { useEffect } from 'react' function Navigation() { const { setOpen } = useContext(NavigationContext)! const pathname = usePathname() useEffect(() => { setOpen(false) }, [pathname, setOpen]) return (/* navigation */) } ``` ### Body Scroll Lock ```tsx useEffect(() => { if (open) { document.body.style.overflow = 'hidden' return () => { document.body.style.overflow = 'unset' } } }, [open]) ``` ### Background Click to Close ```tsx <div onClick={() => setOpen(false)} className="fixed inset-0 bg-gray-900/95" > <nav onClick={(e) => e.stopPropagation()}> {/* Prevent click from bubbling to background */} </nav> </div> ``` ## Dependencies - `framer-motion` - Smooth animations - `@headlessui/react` - Disclosure component - `@heroicons/react` - Menu icons - `next/navigation` - Route change detection

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CaullenOmdahl/Nextjs-React-Tailwind-Assistant'

If you have feedback or need assistance with the MCP directory API, please join our Discord server