import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["menu", "overlay", "toggle"]
toggle() {
const isOpen = this.menuTarget.classList.contains('open')
if (isOpen) {
this.close()
} else {
this.open()
}
}
open() {
this.menuTarget.classList.add('open')
this.overlayTarget.classList.remove('hidden')
this.toggleTarget.setAttribute('aria-expanded', 'true')
// Prevent body scroll
document.body.style.overflow = 'hidden'
// Focus first link for keyboard navigation
const firstLink = this.menuTarget.querySelector('a')
if (firstLink) firstLink.focus()
}
close() {
this.menuTarget.classList.remove('open')
this.overlayTarget.classList.add('hidden')
this.toggleTarget.setAttribute('aria-expanded', 'false')
// Restore body scroll
document.body.style.overflow = ''
}
closeOnEscape(event) {
if (event.key === 'Escape') {
this.close()
}
}
closeOnResize() {
if (window.innerWidth >= 768) {
this.close()
}
}
}
<nav class="navbar"
data-controller="mobile-nav"
data-action="resize@window->mobile-nav#closeOnResize keydown@window->mobile-nav#closeOnEscape">
<div class="container mx-auto px-4">
<div class="flex items-center justify-between h-16">
<%= link_to "MyApp", root_path, class: "text-xl font-bold" %>
<!-- Mobile menu button -->
<button type="button"
class="md:hidden"
data-mobile-nav-target="toggle"
data-action="mobile-nav#toggle"
aria-expanded="false"
aria-label="Toggle navigation">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<!-- Desktop menu -->
<div class="hidden md:flex items-center gap-6">
<%= link_to "Posts", posts_path, class: "nav-link" %>
<%= link_to "About", about_path, class: "nav-link" %>
<% if current_user %>
<%= link_to "Profile", profile_path, class: "nav-link" %>
<%= button_to "Sign Out", destroy_user_session_path, method: :delete, class: "btn btn-sm" %>
<% else %>
<%= link_to "Sign In", new_user_session_path, class: "btn btn-sm" %>
<% end %>
</div>
</div>
</div>
<!-- Mobile menu overlay -->
<div class="fixed inset-0 bg-black bg-opacity-50 z-40 hidden"
data-mobile-nav-target="overlay"
data-action="click->mobile-nav#close"></div>
<!-- Mobile menu drawer -->
<div class="mobile-menu fixed top-0 right-0 bottom-0 w-64 bg-white shadow-xl z-50 transform translate-x-full transition-transform"
data-mobile-nav-target="menu">
<div class="p-6">
<button type="button"
class="mb-6"
data-action="mobile-nav#close"
aria-label="Close menu">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
<div class="flex flex-col gap-4">
<%= link_to "Posts", posts_path, class: "nav-link", data: { action: "mobile-nav#close" } %>
<%= link_to "About", about_path, class: "nav-link", data: { action: "mobile-nav#close" } %>
<% if current_user %>
<%= link_to "Profile", profile_path, class: "nav-link", data: { action: "mobile-nav#close" } %>
<%= button_to "Sign Out", destroy_user_session_path, method: :delete, class: "btn btn-primary w-full" %>
<% else %>
<%= link_to "Sign In", new_user_session_path, class: "btn btn-primary w-full" %>
<% end %>
</div>
</div>
</div>
</nav>
<style>
.mobile-menu.open {
transform: translateX(0);
}
</style>