Menus, Navigation & Active Routes
How to build responsive headers, multi-level dropdowns, and mobile drawers using WpMenu, useWpMenu, and the universal WpLink component.
WordPress themes require flexible navigation systems. In traditional themes, developers struggled with WordPress's legacy wp_nav_menu() walker classes (Walker_Nav_Menu), writing verbose PHP strings just to attach Tailwind utility classes or support dropdown menus.
ForgeWP simplifies navigation into modern React primitives:
- Declarative
<WpMenu />: Renders standard menus and dropdowns with simple CSS class props. - Programmatic
useWpMenu(): Gives you raw access to menu trees for building rich mega-menus and mobile drawers. - Universal
<WpLink />(or<Link />): Provides instant SPA transitions during local development and native Speculation Rules prefetching on your live WordPress site.
1. Declarative Navigation with <WpMenu />
The fastest way to render a navigation bar is with the <WpMenu /> component:
1import { WpMenu, WpLink } from "@forgewp/react"
2
3export function SiteHeader() {
4 return (
5 <header className="sticky top-0 z-50 border-b border-border bg-background/80 backdrop-blur-md">
6 <div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
7 {/* Site Logo */}
8 <WpLink
9 href="/"
10 className="font-heading text-xl font-black tracking-tight text-foreground"
11 >
12 Forge<span className="text-brand">WP</span>
13 </WpLink>
14
15 {/* Primary Navigation Menu */}
16 <WpMenu
17 location="primary"
18 className="hidden items-center gap-8 md:flex"
19 linkClassName="text-sm font-semibold text-muted-foreground transition hover:text-foreground"
20 submenuClassName="absolute top-full mt-2 hidden rounded-xl border border-border bg-card p-2 shadow-lg group-hover:block"
21 />
22
23 {/* Action Button */}
24 <div className="hidden md:block">
25 <WpLink
26 href="/contact"
27 className="bg-brand text-brand-foreground rounded-xl px-4 py-2 text-xs font-bold shadow-sm transition hover:opacity-90"
28 >
29 Get in Touch
30 </WpLink>
31 </div>
32 </div>
33 </header>
34 )
35}<WpMenu /> Properties Reference
Property Type Default Description locationstring"primary"Menu location identifier matching cms/menus.ts and wp.config.ts.classNamestring""CSS classes applied to the outer <nav> container element.linkClassNamestring""CSS classes applied to each top-level and submenu <a> anchor link.submenuClassNamestring""CSS classes applied to nested submenu wrapper containers ( item.children).renderItemfunction— Custom render prop for custom item styling, icons, or badges. itemsWpMenuItem[]— Optional manual items array if bypassing dynamic menu resolution. |
|---|
How do I render multi-level dropdowns or mega-menus?
Each menu item returned by <WpMenu /> or useWpMenu() contains an optional children?: WpMenuItem[] array. If an administrator nests menu items under a parent link in Appearance → Menus, you can check if (item.children?.length) to render a dropdown list or mega-menu panel.
2. Programmatic Menus with useWpMenu()
For advanced use cases—such as a full-screen mobile menu drawer or an animated mega-menu—use the useWpMenu() hook to access the raw menu items tree:
1import { useState } from "react"
2import { useWpMenu, WpLink, type WpMenuItem } from "@forgewp/react"
3import { Menu, X, ChevronDown } from "lucide-react"
4
5export function MobileMenuDrawer() {
6 const [isOpen, setIsOpen] = useState(false)
7 const { items } = useWpMenu("primary")
8
9 return (
10 <div className="md:hidden">
11 {/* Drawer Toggle Button */}
12 <button
13 onClick={() => setIsOpen(!isOpen)}
14 className="rounded-lg border border-border bg-card p-2 text-foreground"
15 aria-label="Toggle Navigation Menu"
16 >
17 {isOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
18 </button>
19
20 {/* Drawer Overlay */}
21 {isOpen && (
22 <div className="fixed inset-x-0 top-16 bottom-0 z-50 overflow-y-auto border-b border-border bg-background/95 p-6 backdrop-blur-lg">
23 <nav className="flex flex-col gap-4">
24 {items.map((item: WpMenuItem) => (
25 <div key={item.id} className="border-b border-border/50 pb-3">
26 <WpLink
27 href={item.url}
28 onClick={() => setIsOpen(false)}
29 className="hover:text-brand font-heading text-lg font-bold text-foreground"
30 >
31 {item.title}
32 </WpLink>
33
34 {/* Submenu links */}
35 {item.children && item.children.length > 0 && (
36 <div className="mt-2 ml-4 flex flex-col gap-2">
37 {item.children.map((child: WpMenuItem) => (
38 <WpLink
39 key={child.id}
40 href={child.url}
41 onClick={() => setIsOpen(false)}
42 className="text-sm font-medium text-muted-foreground hover:text-foreground"
43 >
44 {child.title}
45 </WpLink>
46 ))}
47 </div>
48 )}
49 </div>
50 ))}
51 </nav>
52 </div>
53 )}
54 </div>
55 )
56}The WpMenuItem TypeScript Interface
1export interface WpMenuItem {
2 id: number
3 title: string
4 url: string
5 target?: string // e.g. "_blank"
6 classes?: string[] // CSS classes assigned in WP admin (Appearance → Menus)
7 children?: WpMenuItem[] // Nested sub-items
8}3. Universal Navigation with <WpLink />
Instead of raw HTML <a> tags, use <WpLink /> (or its alias <Link />) for all internal links:
1import { WpLink, Link } from "@forgewp/react"
2;<WpLink href="/about" className="font-semibold">
3 About Us
4</WpLink>The Three-Mode Link Architecture
<WpLink> automatically alters its behavior based on runtime context:
- In Local Development (Vite SPA): Intercepts standard left-clicks and performs instant client-side route transitions using the HTML5 History API without reloading the page.
- In Production WordPress: Renders clean, standard
<a>tags. Modern browsers utilize WordPress Speculation Rules to prefetch the linked pages in the background, achieving instantaneous page loads! - In the Gutenberg Editor Preview: Intercepts clicks to prevent content editors from accidentally navigating away from the WordPress admin editing canvas.
4. Active Route Detection with useLocation()
To highlight the active navigation item when visitors browse your site, use the useLocation() hook:
1import { WpLink, useLocation } from "@forgewp/react"
2
3export function ActiveNavLink({
4 href,
5 label,
6}: {
7 href: string
8 label: string
9}) {
10 const [pathname] = useLocation()
11 const isActive =
12 pathname === href || (href !== "/" && pathname.startsWith(href))
13
14 return (
15 <WpLink
16 href={href}
17 className={`text-sm font-semibold transition ${
18 isActive
19 ? "text-brand border-brand border-b-2 pb-1 font-bold"
20 : "text-muted-foreground hover:text-foreground"
21 }`}
22 >
23 {label}
24 </WpLink>
25 )
26}5. Defining & Registering Menu Locations
A. Local Development Mock Menus (cms/menus.ts)
During local Vite development, ForgeWP populates menus from cms/menus.ts using the type-safe defineWpMenus helper:
1import { defineWpMenus } from "@forgewp/react"
2
3export const menus = defineWpMenus({
4 primary: [
5 { id: 1, title: "Home", url: "/" },
6 { id: 2, title: "Blog", url: "/blog" },
7 {
8 id: 3,
9 title: "Services",
10 url: "/services",
11 children: [
12 { id: 31, title: "Web Development", url: "/services/web" },
13 { id: 32, title: "UI/UX Design", url: "/services/design" },
14 ],
15 },
16 { id: 4, title: "Contact", url: "/contact" },
17 ],
18 footer: [
19 { id: 5, title: "Privacy Policy", url: "/privacy" },
20 { id: 6, title: "Terms of Service", url: "/terms" },
21 ],
22})
23
24export default menusB. Automatic WordPress Admin Registration
In ForgeWP, menu location registration is automatic and zero-config:
- Automatic Registration: The compiler inspects your
cms/menus.tskeys (primary,footer, etc.) and automatically emits standard WordPressregister_nav_menus()calls insidefunctions.php:
1register_nav_menus(array(
2 'primary' => __('Primary Navigation', 'theme-textdomain'),
3 'footer' => __('Footer Navigation', 'theme-textdomain'),
4 ));Site administrators can immediately assign menus to these locations inside the WordPress admin under Appearance → Menus!
- Optional Admin Label Customization (
wp.config.ts): If you want to customize the human-readable description shown in the WordPress admin (for example, displaying"Main Header Navigation"instead of"Primary Navigation"), you can optionally add asettings.menusblock inwp.config.ts(see The wp.config.ts File):
1// Add this section inside your existing defineConfig({ ... }) object.
2 export default defineConfig({
3 // Keep your existing theme configuration here.
4 // ADD THIS SECTION only when you want custom admin labels.
5 settings: {
6 menus: {
7 primary: { description: "Main Header Navigation" },
8 footer: { description: "Footer Utility Links" },
9 },
10 },
11 })Do I have to declare menus in `wp.config.ts`?
No! Declaring menus in wp.config.ts is strictly optional. By default, ForgeWP automatically registers all menu locations found in cms/menus.ts with sensible display names. You only need wp.config.ts if you wish to customize the admin display label.
Next Step: Site Options & Custom Fields
Now that your theme navigation is fully configured, discover how to access global site metadata, theme customizer settings, and ACF custom fields: