Whakerexa > Menus

Menus

wexa.css gives nav.nav-wexa its base styles: colors, shadow, and a horizontal flex layout sticky at the top of the page. menu.css extends this with fixed-position layout classes, collapsible behavior, and off-canvas submenus. Basic layouts require no JavaScript. The companion MenuManager class adds the interactive behaviors: pin, mobile toggle, and off-canvas submenu open/close.

Default nav — wexa.css

A nav.nav-wexa without any layout class is horizontal and sticky at the top of the page. wexa.css applies nav colors (--nav-bg-color, --nav-fg-color), shadow, and flex layout. menu.css adds item sizing and min-height. No layout class, no JavaScript required.

<!-- wexa.css + menu.css, no layout class: horizontal, sticky at top -->
<nav class="nav-wexa" aria-label="Main navigation">
    <a class="menuitem" href="#">Home</a>
    <a class="menuitem" href="#" aria-current="page">Current</a>
</nav>

Variables

Variable Role
--nav-bg-color Nav background color (solid color or gradient)
--nav-fg-color Nav foreground color (text and icon)

Layout classes

Add one class to nav.nav-wexa to fix its position on the page. The body receives a matching padding automatically via body:has(nav.nav-wexa.xxx) selectors. No JavaScript required for static layouts.

Live demos: Side menu · Bottom + top menu

See live demos linked above.

<!-- Fixed top -->
<nav class="nav-wexa top" aria-label="Main navigation">...</nav>

<!-- Fixed bottom -->
<nav class="nav-wexa bottom" aria-label="Main navigation">...</nav>

<!-- Fixed left side, always expanded -->
<nav class="nav-wexa side" aria-label="Main navigation">...</nav>

<!-- Fixed left side, collapsible on hover/focus -->
<nav id="nav-content" class="nav-wexa side collapsible"
     aria-label="Main navigation">
    <a class="menuitem" href="#">
        <svg aria-hidden="true">...</svg>
        <span>Home</span>
    </a>
</nav>

Header nav — #header-nav-content

The special ID header-nav-content places a compact icon-only nav at the top-right of the header (absolute positioned, no shadow, no background). Use it for small accessibility or branding controls inside a decorative header.

See the side menu demo header for a live example.

<header>
    <nav id="header-nav-content" aria-label="Accessibility navigation">
        <button class="menuitem" aria-label="..."></button>
    </nav>
</header>

Variables

Variable Default Role
--nav-min-height max(calc(4rem / var(--size-divisor)), 3rem) Height of top/bottom nav bar
--nav-min-width 0 Collapsed width of side nav (set > 0 for side menus)
--nav-expanded-width max(calc(16rem / var(--size-divisor)), 14rem) Side nav width when expanded or pinned
--nav-item-width-v-collapsed var(--nav-min-width) Item width when side nav is collapsed
--nav-item-width-v-expanded var(--nav-expanded-width) Item width when side nav is expanded
--nav-item-height max(calc(2.5rem / var(--size-divisor)), 2.5rem) Default height of any menu item
--menu-button-size max(calc(4rem / var(--size-divisor)), 3rem) Size of the #menu-button hamburger button

Menuitems and inline submenus

Apply role="menuitem" or the .menuitem class to any interactive element inside nav.nav-wexa. Both target identical styles. Items typically combine an <svg> icon with a <span> label. Use aria-current="page" to mark the active item.

<nav class="nav-wexa ..." aria-label="Main navigation">
    <a class="menuitem" href="#">
        <svg aria-hidden="true">...</svg>
        <span>Back</span>
    </a>
    <button class="menuitem" type="button">
        <svg aria-hidden="true">...</svg>
        <span>Help</span>
    </button>
    <a class="menuitem" href="#" aria-current="page">
        <span>Current</span>
    </a>
</nav>

Grouping with <section>

Items inside a side nav can be grouped in <section> elements. Sections stack vertically with automatic spacing between them. Exception: aside.appmenu must never be placed inside a section — it must be a direct child of <nav>.

Inline submenus

For horizontal menus (.top, default), wrap items in <ul><li> and add .submenu to the nested <ul>. The submenu appears on hover or keyboard :focus-within — no JavaScript required. Add .expand to the parent item to show a indicator.

Background color helpers: .submenu-gray, .submenu-red, .submenu-green, .submenu-orange.

Inline submenus only activate when the nav is a direct child of <body> (CSS selector: body > nav > * *:hover .submenu).

<nav class="nav-wexa top" aria-label="Main navigation">
    <a class="menuitem" href="#">Home</a>
    <ul>
        <li>
            <a class="expand menuitem" href="#">Products</a>
            <ul class="submenu submenu-gray">
                <li><a role="menuitem" href="#">Item A</a></li>
                <li><a role="menuitem" href="#">Item B</a></li>
            </ul>
        </li>
        <li><a class="menuitem" href="#">About</a></li>
    </ul>
</nav>

MenuManager — JavaScript

Requires JavaScript. MenuManager handles three interactive behaviors that cannot be achieved with CSS alone.

Mobile toggle — #menu-button

A fixed circular button visible only on screens narrower than 896 px. Place it in <header>; wire it with initMobileToggle(). Manages aria-expanded on the button and .expanded on the side nav.

See live demo: Side menu

<!-- In <header> -->
<button id="menu-button" aria-controls="nav-content"
        aria-expanded="false" aria-label="Toggle navigation menu">
    <svg viewBox="0 0 24 24" ...>
        <line x1="3" y1="6"  x2="21" y2="6"/>
        <line x1="3" y1="12" x2="21" y2="12"/>
        <line x1="3" y1="18" x2="21" y2="18"/>
    </svg>
</button>

Pin button — #pin-menu

Optional. Place inside the side nav. Wired by initSideMenu(). When activated, sets aria-pinned="true" on the nav and keeps it expanded. Hidden on narrow screens (below 896 px).

See live demo: Side menu

<nav id="nav-content" class="nav-wexa side collapsible">
    <button id="pin-menu" class="menuitem menu-pin"
            type="button" aria-controls="nav-content"
            aria-pressed="true" aria-label="Pin menu">
        <svg aria-label="Pushpin">...</svg>
    </button>
    ...
</nav>

Off-canvas panels — aside.appmenu

An aside.appmenu slides in from an edge when its toggle button is activated. Registered with registerSubmenu(asideId, toggleId).

Position each panel with CSS variables on the aside element: --appmenu-position (left | right | top | bottom) and --appmenu-align (two keywords: horizontal then vertical, e.g. left top). MenuManager reads these and sets the corresponding data-submenu-position and data-submenu-align-* attributes, which the CSS then targets.

The aside must be a direct child of <nav>, never wrapped in a <section>: background: inherit would otherwise inherit the section's transparent background instead of the nav's.

Use .nav-section-label on a <p> inside the appmenu nav to display a non-interactive section heading.

Live demos: Side menu · Bottom menu

<nav id="nav-content" class="nav-wexa side collapsible">

    <button class="menuitem" id="toggle-settings"
            aria-haspopup="true" aria-controls="appmenu-settings">
        <svg aria-hidden="true">...</svg>
        <span>Settings</span>
    </button>

    <!-- Direct child of nav — never inside <section> -->
    <aside id="appmenu-settings" class="appmenu"
           style="--appmenu-position: left; --appmenu-align: left top;"
           aria-label="Settings submenu">
        <nav>
            <p class="nav-section-label">Settings</p>
            <a class="menuitem" href="#">Profile</a>
            <a class="menuitem" href="#">Preferences</a>
        </nav>
    </aside>

</nav>

const menu = new MenuManager();
menu.initSideMenu();
menu.initMobileToggle();
menu.registerSubmenu('appmenu-settings', 'toggle-settings');

Variables — appmenu

Variable Default Role
--appmenu-position left Edge from which the panel slides in (left | right | top | bottom)
--appmenu-align left top Inner alignment of items (horizontal keyword then vertical keyword)