ForgeWP

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:

src/components/SiteHeader.tsx
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}
Table · 4 columns
Property
Type
Default
Description
location
string
"primary"
Menu location identifier matching cms/menus.ts and wp.config.ts.
className
string
""
CSS classes applied to the outer <nav> container element.
linkClassName
string
""
CSS classes applied to each top-level and submenu <a> anchor link.
submenuClassName
string
""
CSS classes applied to nested submenu wrapper containers (item.children).
renderItem
function
—
Custom render prop for custom item styling, icons, or badges.
items
WpMenuItem[]
—
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:

src/components/MobileMenuDrawer.tsx
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

WpMenuItem Type Definition
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}

Instead of raw HTML <a> tags, use <WpLink /> (or its alias <Link />) for all internal links:

Universal Navigation with WpLink
1import { WpLink, Link } from "@forgewp/react"
2;<WpLink href="/about" className="font-semibold">
3  About Us
4</WpLink>

<WpLink> automatically alters its behavior based on runtime context:

  1. 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.
  2. 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!
  3. 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:

src/components/ActiveNavLink.tsx
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:

cms/menus.ts
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 menus

B. Automatic WordPress Admin Registration

In ForgeWP, menu location registration is automatic and zero-config:

  1. Automatic Registration: The compiler inspects your cms/menus.ts keys (primary, footer, etc.) and automatically emits standard WordPress register_nav_menus() calls inside functions.php:
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!

  1. 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 a settings.menus block in wp.config.ts (see The wp.config.ts File):
wp.config.ts (Optional Override)
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:

Site Options, Theme Mods & Custom Fields