javascript erb 102 lines · 2 tabs

Accessible modal dialogs with Stimulus and ARIA

Jordan Lee Jan 2026
2 tabs
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["container", "dialog"]

  connect() {
    this.previousActiveElement = null
  }

  open() {
    this.previousActiveElement = document.activeElement

    this.containerTarget.classList.remove('hidden')
    document.body.style.overflow = 'hidden'

    // Focus the first focusable element in the modal
    const firstFocusable = this.dialogTarget.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    if (firstFocusable) firstFocusable.focus()

    // Trap focus
    document.addEventListener('focusin', this.trapFocus.bind(this))
  }

  close() {
    this.containerTarget.classList.add('hidden')
    document.body.style.overflow = ''

    // Return focus to trigger element
    if (this.previousActiveElement) {
      this.previousActiveElement.focus()
    }

    document.removeEventListener('focusin', this.trapFocus.bind(this))
  }

  trapFocus(event) {
    if (!this.dialogTarget.contains(event.target)) {
      event.preventDefault()
      const firstFocusable = this.dialogTarget.querySelector(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      )
      if (firstFocusable) firstFocusable.focus()
    }
  }

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

  closeOnBackdrop(event) {
    if (event.target === this.containerTarget) {
      this.close()
    }
  }
}
2 files · javascript, erb Explain with highlit

Modals are everywhere but often fail accessibility requirements. I build modals with Stimulus that properly manage focus, support keyboard navigation, and announce themselves to screen readers. When opened, focus moves to the modal and gets trapped inside using focusin events. Escape key closes the modal and returns focus to the trigger element. ARIA attributes like role='dialog', aria-modal='true', and aria-labelledby provide semantic meaning for assistive technology. I also prevent body scroll when the modal is open and provide both click-outside and explicit close button dismissal. This pattern ensures modals work for all users regardless of how they interact with the page.


Related snips

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semantic HTML Example</title>

Semantic HTML5 elements and accessibility best practices

html html5 semantics
by Alex Chang 2 tabs
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

Share this code

Here's the card — post it anywhere.

Accessible modal dialogs with Stimulus and ARIA — share card
Link copied