npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jasonrundell/react-mega-menu

v3.0.0

Published

A React project which aims to be an accessible, responsive, boilerplate top navigation menu with a "Mega Menu"!

Readme

React Mega Menu

An accessible, responsive top navigation menu for React with a "Mega Menu", styled by a shipped stylesheet and design tokens rather than CSS-in-JS.

v3 runs on React 19 and Topiary design tokens. If you are on React 18, stay on the v2.x line (see Migrating from v2).

Features

  • WCAG 2.1 AA compliant, W3C valid markup
  • Fly-out mega panels and nested sub-panels from a single config object
  • Full keyboard support: Tab order, Enter / Space to open, Escape to close and return focus, outside click to close
  • Responsive: an off-canvas nav with a hamburger toggle below the large breakpoint, a horizontal bar above it
  • Plain CSS, no runtime styling library: one stylesheet, every value routed through a documented --rmm-* token layer that defaults to Topiary tokens with hardcoded fallbacks
  • Re-skins with data-theme across all four Topiary themes from identical markup
  • Configurable slide-in side for the mobile nav (slideDirection)
  • Animations respect prefers-reduced-motion
  • Ships its own TypeScript declarations
  • Automated axe gate and a recorded keyboard walkthrough in the test suite
  • Two demos: a Vite SPA and a Next.js App Router app proving server-side rendering with no unstyled flash

Install

npm install @jasonrundell/react-mega-menu @jasonrundell/topiary react react-dom

Peer dependencies (not bundled, you install them):

| Package | Range | | ----------------------- | --------- | | react | ^19.0.0 | | react-dom | ^19.0.0 | | @jasonrundell/topiary | ^4.1.0 |

Setup

Import both stylesheets once, at your app root, in this order: Topiary's tokens first, then the menu's stylesheet, which reads those tokens. Then set data-theme on an ancestor of the menu to pick a Topiary theme (hangar, broadsheet, arcade or cascade).

// main.tsx (Vite) or app/layout.js (Next.js App Router)
import '@jasonrundell/topiary/style.css'
import '@jasonrundell/react-mega-menu/style.css'
import { Menu } from '@jasonrundell/react-mega-menu'

export const App = () => (
  <div data-theme="hangar">
    <Menu config={config} />
    {/* page content */}
  </div>
)

The menu never imports CSS as a side effect of its JavaScript, so it renders styled on the very first server-rendered paint in Next.js. Without Topiary's stylesheet every token falls back to a hardcoded value, so the menu still renders correctly; it just will not follow your theme.

Usage

Menu takes a config describing the top bar and the item tree. Items are one of four types: main (a top-level link), mega (a top-level item that opens a panel of child items), link (a plain item inside a panel) and sub (a panel item that opens a nested panel of its own).

import { Menu } from '@jasonrundell/react-mega-menu'
import type { MenuConfigShape } from '@jasonrundell/react-mega-menu'

const config: MenuConfigShape = {
  topbar: {
    id: 'topbar',
    logo: { src: '/logo.svg', alt: 'Acme', rel: 'home' },
    title: 'Acme'
  },
  menu: {
    items: [
      { id: 'home', label: 'Home', type: 'main', url: '/' },
      {
        id: 'store',
        label: 'Store',
        type: 'mega',
        url: '/store/',
        items: [
          {
            id: 'deals',
            label: 'Deals',
            type: 'link',
            url: '/store/deals/',
            description: 'Optional one-line description'
          },
          {
            id: 'outdoors',
            label: 'Outdoors',
            type: 'sub',
            url: '/store/outdoors/',
            items: [
              {
                id: 'tools',
                label: 'Tools',
                type: 'link',
                url: '/store/outdoors/tools/'
              }
            ]
          }
        ]
      }
    ]
  }
}

export const Header = () => <Menu config={config} slideDirection="right" />

Props

| Prop | Type | Default | Description | | ---------------- | ------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------- | | config | MenuConfigShape | a built-in sample config | The top bar and item tree (above). | | slideDirection | 'left' \| 'right' | 'left' | Which side the off-canvas mobile nav and its mega / sub panels slide in from. Both directions honor reduced motion. | | id | string | 'rmm__menu' | Id of the menu shell; inner regions derive theirs from it (see Element ids). | | className | string | | Appended to the shell's rmm__menu class. Use it to scope token overrides. |

Any other div attribute (data-*, aria-*, event handlers) is passed through to the shell.

Theming

All of the menu's styling lives in the shipped stylesheet and consumes only the --rmm-* custom properties below. Each one defaults to a Topiary token and carries a hardcoded fallback, so you can restyle the menu on its own without touching the Topiary tokens that drive the rest of your app.

Override tokens on the shell (or any ancestor). Scope them with className:

.site-menu {
  --rmm-link-color: hotpink;
  --rmm-menu-height-large: 3rem;
}
<Menu config={config} className="site-menu" />

The rmm__* class names on every element are stable public API too, so selector-level overrides keep working across releases.

Color

| Token | Purpose | Default | | --------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------- | | --rmm-menu-bg | Menu shell background | var(--topiary-color-surface, #fefefe) | | --rmm-menu-text | Primary text color | var(--topiary-color-text, #1a1a1a) | | --rmm-link-color | Nav item link color | var(--topiary-color-primary, #0066cc) | | --rmm-link-hover-bg | Nav item link :hover / :focus-visible background | var(--topiary-color-surfaceAlt, #f0f0f0) | | --rmm-text-muted | Item description text color | var(--topiary-color-textMuted, #666666) | | --rmm-focus-ring | Composed :focus-visible outline (width, style, color) | var(--topiary-borderWidth-thick, 2px) solid var(--topiary-color-focus, currentColor) |

Panel chrome

| Token | Purpose | Default | | --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | --rmm-panel-bg | Off-canvas nav, mega and sub panel background | var(--topiary-color-surface, #fefefe) | | --rmm-panel-border | Composed border on the shell bottom, panel edges and headings | var(--topiary-borderWidth-hairline, 1px) solid var(--topiary-color-border, #d9d9d9) | | --rmm-panel-shadow | Mega panel drop shadow at the large breakpoint | var(--topiary-shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.12)) | | --rmm-panel-padding | Mega panel inner padding at the large breakpoint | var(--topiary-space-lg, 1.5rem) | | --rmm-link-radius | Link hover pill and hamburger slice corner radius | var(--topiary-radius-md, 3px) | | --rmm-icon-stroke | Chevron stroke width | var(--topiary-borderWidth-thin, 2px) |

Stacking order

| Token | Purpose | Default | | --------------- | ------------------------------------- | ---------------------------------- | | --rmm-menu-z | Fixed menu shell | var(--topiary-zIndex-nav, 9000) | | --rmm-panel-z | Mega / sub panel, above sibling lists | var(--topiary-zIndex-overlay, 2) | | --rmm-list-z | Base nav list layer | var(--topiary-zIndex-base, 1) |

Structural sizing

| Token | Purpose | Default | | ------------------------- | --------------------------------------------------------- | ------------------------------------------ | | --rmm-menu-height | Mobile menu shell height | var(--rmm-topbar-height) | | --rmm-menu-height-large | Menu shell height at the large breakpoint | var(--topiary-space-3xl, 4rem) | | --rmm-topbar-height | Top bar height | var(--topiary-space-3xl, 4rem) | | --rmm-gutter | Horizontal padding of the shell and every panel | var(--topiary-space-lg, 1.5rem) | | --rmm-column-min-width | Minimum mega panel column width at the large breakpoint | calc(var(--topiary-space-3xl, 4rem) * 3) | | --rmm-logo-height | Logo image height | var(--topiary-space-xl, 2rem) | | --rmm-hamburger-width | Hamburger slice container width | var(--topiary-space-xl, 2rem) | | --rmm-hamburger-height | Hamburger slice container height | var(--topiary-space-lg, 1.25rem) |

Spacing scale

| Token | Purpose | Default | | ---------------- | ------------------------------------- | ----------------------------------- | | --rmm-space-xs | Extra-small margins, gaps and padding | var(--topiary-space-2xs, 0.25rem) | | --rmm-space-sm | Small margins, gaps and padding | var(--topiary-space-xs, 0.5rem) | | --rmm-space-md | Medium margins, gaps and padding | var(--topiary-space-md, 1rem) | | --rmm-space-xl | Extra-large margins, gaps and padding | var(--topiary-space-xl, 2rem) |

Typography

| Token | Purpose | Default | | --------------------------------- | -------------------------------- | --------------------------------------- | | --rmm-font-family | Body font stack | var(--topiary-font-body, inherit) | | --rmm-line-height | Link and description line height | var(--topiary-lineHeight-normal, 1.5) | | --rmm-title-font-size | Top bar title | var(--topiary-fontSize-xl, 1.5rem) | | --rmm-nav-item-font-size | Nav, mega and sub item link text | var(--topiary-fontSize-md, 1rem) | | --rmm-description-font-size | Item description | var(--topiary-fontSize-xs, 0.75rem) | | --rmm-hamburger-label-font-size | Hamburger visible label | var(--topiary-fontSize-md, 1rem) | | --rmm-font-weight-bold | Hamburger label weight | var(--topiary-fontWeight-bold, 700) |

Motion

| Token | Purpose | Default | | ------------------------------------------- | ------------------------------------- | --------------------------------------------- | | --rmm-motion-duration | Slide open / close animation duration | var(--topiary-duration-slow, 0.75s) | | --rmm-motion-easing | General transition easing | var(--topiary-easing-standard, ease-in-out) | | --rmm-hamburger-transition-duration | Hamburger icon morph duration | var(--topiary-duration-normal, 0.5s) | | --rmm-hamburger-slice-transition-duration | Individual hamburger slice transition | var(--topiary-duration-fast, 0.25s) |

The source of truth for this table is src/styles/style.css; the test suite fails if the two drift.

Element ids

Every element the menu renders carries a stable rmm__* id and class name, so existing stylesheets and tests keep working. Passing your own id to Menu changes the ids of the shell and its two inner regions only, and never puts the same id on more than one element:

| Element | Default id | With id="site-menu" | | ------------------------------- | ---------------- | --------------------- | | Menu shell (div.rmm__menu) | rmm__menu | site-menu | | Off-canvas nav (nav.rmm__nav) | rmm__nav | site-menu__nav | | Main list (ul.rmm__nav-list) | rmm__main | site-menu__main | | Hamburger button | rmm__hamburger | rmm__hamburger |

The Hamburger's aria-controls always points at the nav's rendered id, custom or default. The top bar, title, Hamburger and menu items keep their fixed rmm__* ids, so do not reuse one of those as the custom id.

TypeScript

The package ships its own declaration file (dist/index.d.ts), so import { Menu } from '@jasonrundell/react-mega-menu' typechecks with no extra @types package or local shim. Menu's props are typed, and the config shape is exported so you can annotate your own config object:

import { Menu } from '@jasonrundell/react-mega-menu'
import type { MenuConfigShape } from '@jasonrundell/react-mega-menu'

const config: MenuConfigShape = {
  topbar: {
    id: 'topbar',
    logo: { src: '/logo.svg', alt: 'Logo' },
    title: 'Site'
  },
  menu: { items: [{ id: 'home', label: 'Home', type: 'main', url: '/' }] }
}

export const Header = () => <Menu config={config} slideDirection="right" />

Also exported: MenuProps, MenuConfigItem, MenuConfigTopbar and the MenuItemType union ('main' | 'link' | 'mega' | 'sub').

Migrating from v2

v3 is a styling-architecture release. Your Menu usage stays the same; what changes is what you install and import.

What you must change

  1. Upgrade to React 19: the peer dependency is react / react-dom ^19.0.0.
  2. Remove @emotion/react and @emotion/styled from your dependencies. v3 does not use them.
  3. Add @jasonrundell/topiary (^4.1.0) as a dependency.
  4. Import the two stylesheets once at your app root, Topiary's first (see Setup), and set data-theme on an ancestor of the menu.
  5. If you had custom CSS targeting Emotion's generated class names, move it to --rmm-* token overrides or to the stable rmm__* classes (see Theming).

What stays the same

  • The Menu component, its config shape and the id / className props. No config or markup changes are required beyond dependencies, stylesheets and React 19.
  • Every rmm__* id and class name, so existing selectors and tests keep working.
  • Keyboard behavior, Escape and outside-click handling, the hamburger and off-canvas nav on mobile, and prefers-reduced-motion support.

What is new

  • slideDirection ('left' by default, matching v2's behavior).
  • Shipped TypeScript declarations.
  • A custom id no longer lands on three elements; inner regions derive unique ids from it. If you passed a custom id in v2 and targeted it on the nav or main list, point those selectors at <id>__nav and <id>__main instead.
  • At mobile width the closed off-canvas nav is inert, so keyboard users no longer tab through hidden links.
  • The stylesheet paints the panels itself. In v2 the off-canvas nav, mega and sub panels were transparent until your own CSS gave them a background; v3 paints them from --rmm-panel-bg and friends (see Theming), lays mega panels out as a grid of columns, hides the mobile-only "back" rows on large screens and draws the chevrons in CSS. Restyle through the tokens rather than re-adding selector overrides.

Staying on v2

The v2.x line is the React 18 / Emotion line and is in maintenance only: it receives no new features. Pin @jasonrundell/react-mega-menu@^2 if you are not ready for React 19.

Accessibility

The rendered menu is scanned with axe in every state (closed, open, sub-panel open, mobile and desktop) as part of npm test, and a manual keyboard walkthrough is recorded in docs/accessibility/keyboard-walkthrough.md with a re-runnable browser harness. See that document before each release.

Reduced motion

The slide open / close animation is suppressed under prefers-reduced-motion: reduce. Learn more:

How to Test prefers-reduced-motion on macOS

  1. Open settings for Accessibility
  2. Toggle Reduce Motion On/Off

How to Test prefers-reduced-motion on iOS

  1. Open settings for Accessibility
  2. Toggle Reduce Motion On/Off

How to Test prefers-reduced-motion on Windows 10

  1. Press the Win+R keys to open Run, type SystemPropertiesPerformance.exe into Run, and click/tap on OK to directly open to the Visual Effects tab in Performance Options.
  2. Check (enable - default) or uncheck (disable) Animate controls and elements inside windows.
  3. If you don't see an immediate change, then you can restart the explorer process or sign out and sign in to apply instead.

How to Test prefers-reduced-motion on Android

  1. Search in your system settings for Remove Animations and toggle On/Off, or
  2. Go to your system settings > Accessibility and look for a toggle to reduce motion or turn off animations
  3. If you have a browser app already open, you'll have to force quit it to have the setting take effect

Demos

Both demos install the package from the tarball committed at the repo root (npm run pack:demo rebuilds it).

Deploy

Deploy with Vercel

Development

npm install
npm test            # builds, then runs the Jest suite (axe, contracts, fixtures)
npm run build       # dist/index.es.js, dist/index.cjs, dist/style.css, dist/index.d.ts, dist/index.d.cts
npm run pack:demo   # rebuilds the tarball both demos install from

The design notes for v3 live in docs/refactor/topiary-migration.md and the Topiary audit in docs/refactor/topiary-gap-report.md.

Special Thanks

Donna Vitan for the accessibility consultation

Resources

Icons

Icons from the Free for Web download pack by Font Awesome