javascript erb 120 lines · 2 tabs

Mobile-first responsive navigation with Stimulus

Jordan Lee Jan 2026
2 tabs
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()
    }
  }
}
2 files · javascript, erb Explain with highlit

Mobile navigation requires different patterns than desktop—hamburger menus, slide-out drawers, and touch-friendly interactions. I build a responsive nav with Stimulus that shows a mobile menu button below a breakpoint and auto-hides when links are clicked. The controller handles opening/closing animations, focus trapping for accessibility, and escape key handling. On wider screens, CSS shows the full horizontal nav and Stimulus gracefully does nothing. This progressive enhancement approach ensures the nav works even if JavaScript fails—the mobile menu button falls back to a traditional anchor that scrolls to the nav. Touch gestures like swipe-to-close add polish for mobile users.


Related snips

erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"

export default class extends Controller {
  connect() {
    // Global shortcuts

Keyboard shortcuts with Stimulus and Mousetrap

stimulus javascript ux
by Jordan Lee 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab
javascript
import { Application } from "@hotwired/stimulus"
import FormSubmitController from "./controllers/form_submit_controller"

const application = Application.start()
application.debug = false

Disable submit button while Turbo form is submitting

rails hotwire stimulus
by codesnips 3 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["modal", "message", "confirm", "cancel"]
  static values = {
    message: String

Turbo confirmation dialogs with custom modal

hotwire turbo stimulus
by Jordan Lee 3 tabs

Share this code

Here's the card — post it anywhere.

Mobile-first responsive navigation with Stimulus — share card
Link copied