Skip to content

Feature: Mobile-Responsive Hamburger Navigation #91

Description

@AgentKush

Summary

The site header has no mobile-responsive navigation. On smaller screens, the nav buttons (Mods, Tools, Info) stack awkwardly below the logo with no hamburger menu or collapsible drawer. This makes the site feel broken on phones and small tablets.

Current Behavior

Looking at app/views/layouts/_header.html.erb:

<div class="... md:pt-4 md:pb-12 md:px-5 md:flex md:flex-auto md:items-center md:justify-between">
  <div class="flex items-center justify-start flex-1 min-w-0">
    <%= image_tag "daedalus-logo.png", width: "64", alt: "Daedalus Logo" %>
    <%= link_to "Project Daedalus", home_path, class: "... text-icarus-500 ..." %>
  </div>
  <div class="flex mt-4 md:mt-0 md:ml-4">
    <button type="button" id="mods-button" class="button">
      <%= link_to "Mods", mods_path, class: "... text-icarus-500 hover:text-blue-500" %>
    </button>
    <!-- Tools, Info buttons same pattern -->
  </div>
</div>

Problems on mobile:

  • The nav buttons <div class="flex mt-4 md:mt-0"> just stacks below the logo with mt-4 — no intentional mobile layout
  • No hamburger menu or drawer
  • The header padding (md:pt-4 md:pb-12 md:px-5) only applies at md+ — below that, no padding at all
  • On the mods page, the author dropdown is invisible md:visible — completely hidden on mobile with no alternative
  • Adding a theme toggle button (from the pending PR) would make the header even more crowded on mobile

Proposed Solution

1. Hamburger Button (Mobile Only)

Add a hamburger icon that's only visible below the md breakpoint:

<button type="button"
        data-action="click->navigation#toggle"
        data-navigation-target="button"
        class="md:hidden inline-flex items-center justify-center w-10 h-10 rounded-lg text-icarus-500 hover:text-icarus-400 hover:bg-slate-700/50 transition-colors"
        aria-label="Toggle navigation menu"
        aria-expanded="false">
  <span data-navigation-target="icon" class="text-2xl"></span>
</button>

2. Collapsible Mobile Menu

Wrap the nav links in a container that toggles visibility:

<div data-navigation-target="menu"
     class="hidden md:flex mt-4 md:mt-0 md:ml-4"
     role="navigation"
     aria-label="Main navigation">
  <!-- Existing Mods, Tools, Info buttons -->
  <!-- On mobile: displayed as vertical stack when menu is open -->
</div>

Mobile open state: Full-width dropdown below the header with vertical link stack:

<div class="flex flex-col md:flex-row w-full md:w-auto">
  <a href="/mods" class="px-4 py-3 text-icarus-500 hover:bg-slate-700/50 border-b border-slate-700 md:border-0">Mods</a>
  <a href="/tools" class="px-4 py-3 text-icarus-500 hover:bg-slate-700/50 border-b border-slate-700 md:border-0">Tools</a>
  <a href="/info" class="px-4 py-3 text-icarus-500 hover:bg-slate-700/50 md:border-0">Info</a>
</div>

3. New Stimulus Controller

Create app/javascript/controllers/navigation_controller.js:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["menu", "button", "icon"]

  toggle() {
    const isOpen = !this.menuTarget.classList.contains("hidden")

    if (isOpen) {
      this.close()
    } else {
      this.open()
    }
  }

  open() {
    this.menuTarget.classList.remove("hidden")
    this.buttonTarget.setAttribute("aria-expanded", "true")
    this.iconTarget.textContent = "✕"

    // Close on click outside
    document.addEventListener("click", this.closeOnClickOutside)
    // Close on Escape
    document.addEventListener("keydown", this.closeOnEscape)
  }

  close() {
    this.menuTarget.classList.add("hidden")
    this.buttonTarget.setAttribute("aria-expanded", "false")
    this.iconTarget.textContent = "☰"

    document.removeEventListener("click", this.closeOnClickOutside)
    document.removeEventListener("keydown", this.closeOnEscape)
  }

  closeOnClickOutside = (event) => {
    if (!this.element.contains(event.target)) this.close()
  }

  closeOnEscape = (event) => {
    if (event.key === "Escape") this.close()
  }

  // Auto-close after Turbo navigation
  disconnect() {
    this.close()
  }
}

4. Desktop Layout Unchanged

On md+ screens, everything stays exactly as it is — the hamburger is md:hidden and the nav links container is md:flex. Zero changes to desktop experience.

Files to Modify

File Change
app/views/layouts/_header.html.erb Add hamburger button, wrap nav in data-controller="navigation", restructure for responsive layout
app/javascript/controllers/navigation_controller.js New file — Stimulus controller for menu toggle
app/views/layouts/application.html.erb May need z-index adjustment on <main> so mobile menu overlays content

Design Notes

  • Hamburger icon: text-icarus-500 gold to match nav links
  • Mobile menu background: Same gradient as header (bg-gradient-to-b from-slate-600 via-slate-500 to-slate-200 dark:from-slate-900 dark:via-slate-900 dark:to-slate-800)
  • Menu items: text-icarus-500 hover:text-blue-500 matching existing nav link colors
  • Each item gets py-3 for comfortable 44px+ touch targets
  • Subtle gold dividers between items: border-b border-slate-700
  • Smooth transition: transition-all duration-200 on the menu container
  • If the theme toggle PR is merged, include the toggle button in the mobile menu too

Accessibility

  • Hamburger: aria-label="Toggle navigation menu", aria-expanded toggled by JS
  • Menu container: role="navigation", aria-label="Main navigation"
  • Escape key closes menu
  • Click outside closes menu
  • Focus returns to hamburger button when menu closes
  • Menu closes automatically on Turbo navigation (Stimulus disconnect())

Testing

  • Hamburger icon visible only below md breakpoint
  • Full nav links visible only at md+ (hamburger hidden)
  • Tapping hamburger opens mobile menu with vertical nav links
  • Tapping hamburger again closes menu (icon switches ☰ ↔ ✕)
  • Clicking a nav link navigates and closes menu
  • Clicking outside menu closes it
  • Escape key closes menu
  • aria-expanded updates correctly
  • Menu doesn't shift page content when opening (overlay, not push)
  • Desktop layout completely unchanged
  • Works with Turbo — menu closes after navigation
  • Theme toggle (if merged) accessible in mobile menu

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions