Tailwind UI menu with stimulus.js and Rails
This assumes you've already got a Rails application with Tailwind CSS setup and you're using Tailwind UI.
- Copy the Application layout that you like and update application.html.erb
- Generate the stimulus controller
bin/rails g stimulus menu
That'll create app/javascript/controllers/menu_controller.js
In order to use that controller, we need to mount it to our HTML element we
want to control by adding data-controller="menu"
:
<!-- Profile dropdown -->
<div data-controller="menu" class="ml-3 relative">
<div>
<button type="button" class="max-w-xs bg-white flex items-center text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<span class="sr-only">Open user menu</span>
<img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">
<!-- ... -->
Add the hidden
class to the dropdown menu content:
<div data-controller="menu" class="ml-3 relative">
<!-- ... we're still inside that profile dropdown menu thing, see `hidden` on the next line? -->
<div class=" hidden origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
<a href="/settings" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-1">
Settings
</a>
<a href="/users/sign_out" class="block px-4 py-2 text-sm text-gray-00" role="menuitem" tabindex="-1" id="user-menu-item-2">
Sign out
</a>
</div>
</div>
Now we want to add an action to the button so that when it's clicked, we toggle the hidden
class.
// app/javascript/controllers/menu_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="menu"
export default class extends Controller {
connect() {
console.log("menu controller connected")
}
toggleDropdown() {
// add or remove the `hidden` class from the dropdown content...
}
}
Now we can execute the toggleDropdown
action by adding a data-action
attribute to the button:
<div data-controller="menu" class="ml-3 relative">
<div>
<button data-action="menu#toggleDropdown" type="button" class="max-w-xs bg-white flex items-center text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<span class="sr-only">Open user menu</span>
<img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">
</button>
</div>
<!-- ... -->
Next we need to wire up the target so that the controller knows which element to show or hide.
That's done by adding a data-menu-target="dropdown"
to the content of the dropdown:
<div data-controller="menu" class="ml-3 relative">
<!-- ... we're still inside that profile dropdown menu thing, see `hidden` on the next line? -->
<div data-menu-target="dropdown" class=" hidden origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
<a href="/settings" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-1">
Settings
</a>
<a href="/users/sign_out" class="block px-4 py-2 text-sm text-gray-00" role="menuitem" tabindex="-1" id="user-menu-item-2">
Sign out
</a>
</div>
</div>
And adding it to the list of targets for the controller:
export default class extends Controller {
static targets = ["dropdown"];
// ...
Now we can implement our toggleDropdown
action:
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="menu"
export default class extends Controller {
static targets = ["dropdown"];
connect() {
console.log("menu controller connected")
}
toggleMenu() {
this.dropdownTarget.classList.toggle("hidden")
}
}