ForgeWP

Build a Mobile Menu Island

Create a native disclosure menu and add keyboard behavior through a small React hydration island.

Build a mobile navigation menu that opens with a tap or keyboard activation. Its links are present in the exported HTML; React adds Escape handling and returns focus to the menu control.

An island is an interactive component inside an otherwise server-rendered page. Hydration connects React's event handlers to its existing HTML. See Selective Hydration for the wider model.

Before you start

Use a React-adapter theme. You will create a component and a demonstration Page. Replace the example /rooms/ and /contact/ addresses with published WordPress permalinks, including any installation subdirectory.

The link labels in this example belong to the React source. For editor-managed navigation, connect the component to the menu APIs described in the WordPress Data Layer guides.

1. Create the menu component

Create this file:

src/components/MobileMenu.tsx
1import { useRef } from "react"
2
3export function MobileMenu() {
4  const detailsRef = useRef<HTMLDetailsElement>(null)
5  const summaryRef = useRef<HTMLElement>(null)
6
7  return (
8    <details
9      ref={detailsRef}
10      className="rounded-xl border p-4 md:hidden"
11      onKeyDown={(event) => {
12        if (event.key === "Escape" && detailsRef.current?.open) {
13          event.preventDefault()
14          detailsRef.current.open = false
15          summaryRef.current?.focus()
16        }
17      }}
18    >
19      <summary
20        ref={summaryRef}
21        className="cursor-pointer font-semibold focus-visible:outline-2 focus-visible:outline-offset-4"
22      >
23        Menu
24      </summary>
25      <nav aria-label="Mobile navigation" className="mt-4 flex flex-col gap-4">
26        <a href="/rooms/">Rooms</a>
27        <a href="/contact/">Contact</a>
28      </nav>
29    </details>
30  )
31}

details and summary provide the open/close control, keyboard activation, and expanded state. Keep their native semantics rather than adding an ARIA menu role: these are ordinary navigation links. useRef() gives the Escape handler access to the existing elements without creating them during rendering.

The browser owns the disclosure's open state. This lets visitors use the same control before hydration or with JavaScript disabled. Escape-to-close is the added React behavior.

2. Mount it in a demonstration Page

Create the Page below. Keep the named exports and matching file names so ForgeWP can discover the Page and island.

src/app/pages/NavigationDemo.tsx
1import { Hydrate } from "@forgewp/react"
2import { MobileMenu } from "../../components/MobileMenu"
3
4export function NavigationDemo() {
5  return (
6    <main className="mx-auto max-w-5xl space-y-8 px-6 py-12">
7      <h1 className="text-3xl font-semibold">Explore your stay</h1>
8      <nav aria-label="Desktop navigation" className="hidden gap-6 md:flex">
9        <a href="/rooms/">Rooms</a>
10        <a href="/contact/">Contact</a>
11      </nav>
12      <Hydrate trigger="load">
13        <MobileMenu />
14      </Hydrate>
15      <p>Find your room or get in touch with our team.</p>
16    </main>
17  )
18}

The responsive classes show one navigation at a time. trigger="load" schedules hydration at page load; the rest of this Page needs no React event handlers. Keep DOM access inside handlers, where it runs in the visitor's browser.

3. Export and open the Page

Run pnpm forgewp export from the theme root. Install the exported theme on a WordPress test site, create a Page, and select its Navigation Demo template. Publish it and visit its permalink. If you also want a local React preview, register the Page in your theme's src/app/routes.tsx following its existing route and layout pattern.

This is a Page-level demonstration. Once verified, you can mount the same component in your site's header and remove the demonstration navigation.

4. Verify the result

  • At a narrow viewport, activate Menu with a pointer, Enter, and Space. The links should appear and be reachable with Tab.
  • Open the menu, focus a link, and press Escape. The disclosure should close and focus should return to Menu after hydration.
  • At a desktop width, confirm that only the desktop navigation is visible and keyboard reachable.
  • Disable JavaScript and reload. The native disclosure should still open and its links should work.
  • Check both real destination URLs on WordPress, and inspect the exported HTML for data-forgewp-hydrate="mobile-menu" and data-forgewp-trigger="load".
  • Enable reduced motion in your operating system. This example has no movement animation, so interaction should remain immediate.

Next: practice a modal interaction

Build an arrival-information dialog using another native browser control and a focused React island.