Tutorial
The short-form tutorial below represents how to pair Rails UI, Stimulus.js, and Ruby on Rails together to achieve a dropdown pattern using a set of elements working together.
Dependencies
To add transition effects to components like dropdowns, we prefer an additional dependency called stimulus-use.
If for some reason stimulus-use is not installed (check your package.json file.) run the following.
yarn add stimulus-use
Stimulus.js and stimulus-use are already installed as a dependency of the Hound theme.
1. Initializing a dropdown
Use the data-controller="dropdown" attribute to initialize a new dropdown component using Stimulus.js.
<div data-controller="dropdown">
<button>Dropdown</button>
<div>
<a href="#">Menu list item 1</a>
<a href="#">Menu list item 2</a>
</div>
</div>
yarn add stimulus-use
// app/javascript/controllers/dropdown_controller.js
import { Controller } from '@hotwired/stimulus'
import { useTransition } from 'stimulus-use'
export default class extends Controller {
static targets = ['menu', 'trigger']
connect() {
useTransition(this, {
element: this.menuTarget,
})
}
toggle() {
this.toggleTransition()
}
hide(event) {
if (
!this.element.contains(event.target) &&
!this.menuTarget.classList.contains('hidden')
) {
this.leave()
}
}
}
Be sure the controller is registered inside app/javascript/controllers/index.js. (This should already be the case when you installed the Hound theme.)
// app/javascript/controllers/index.js
import { application } from './application'
import DropdownController from './dropdown_controller.js'
application.register('dropdown', DropdownController)
2. Triggering a dropdown
To trigger a dropdown to appear you will need some form of actionable target like a button or a element. Append the following attributes to your target. data-action="dropdown#toggle click@window->dropdown#hide" (see the base example below for a complete concept).
<div data-controller="dropdown">
<button data-action="click->dropdown#toggle click@window->dropdown#hide">Dropdown</button>
<div>
<a href="#">Menu list item 1</a>
<a href="#">Menu list item 2</a>
</div>
</div>
// app/javascript/controllers/index.js
import { application } from './application'
import DropdownController from './dropdown_controller.js'
application.register('dropdown', DropdownController)
3. Defining the menu
Each dropdown has a corresponding menu that gets hidden by default. The menu requires the following attribute to function data-dropdown-target="menu". The element the target is appended to also requires the class name class='hidden' to be present.
<div data-controller="dropdown">
<button data-action="click->dropdown#toggle click@window->dropdown#hide">Dropdown</button>
<div class="hidden" data-dropdown-target="menu">
<a href="#">Menu list item 1</a>
<a href="#">Menu list item 2</a>
</div>
</div>
// app/javascript/controllers/index.js
import { application } from './application'
import DropdownController from './dropdown_controller.js'
application.register('dropdown', DropdownController)
4. Adding transitions and effects
Stimulus.js doesn't provide help in the context of animation and transitions so we reached for stimulus-use to help.
Using the library you can leverage data attributes to add specific effects provided by Tailwind CSS classes at different states of a dropdown transition.
<div data-controller="dropdown">
<button data-action="click->dropdown#toggle click@window->dropdown#hide">Dropdown</button>
<div
class="hidden"
data-dropdown-target="menu"
data-transition-enter-from="opacity-0 scale-95"
data-transition-enter-to="opacity-100 scale-100"
data-transition-leave-from="opacity-100 scale-100"
data-transition-leave-to="opacity-0 scale-95">
<a href="#">Menu list item 1</a>
<a href="#">Menu list item 2</a>
</div>
</div>
Dropdowns
Base
<div data-controller="dropdown" class="relative inline-block">
<button type="button" data-action="click->dropdown#toggle click@window->dropdown#hide" class="btn btn-primary pr-3">
Dropdown
<%= icon "chevron-down", classes: "w-3 h-3 ml-2" %>
</button>
<div
class="hidden transition transform origin-top-left absolute left-0 top-10 bg-white rounded-lg shadow-xl shadow-slate-900/10 border border-slate-200 md:w-[200px] w-full z-50 py-2 dark:bg-slate-700 dark:shadow-slate-900/50 dark:border-slate-500/60 md:text-sm text-base font-medium text-slate-600 dark:text-slate-200"
data-dropdown-target="menu"
data-transition-enter-from="opacity-0 scale-95"
data-transition-enter-to="opacity-100 scale-100"
data-transition-leave-from="opacity-100 scale-100"
data-transition-leave-to="opacity-0 scale-95"
>
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300">Bookmark</a>
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300">Report</a>
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300">Export</a>
</div>
</div>
<div data-controller="dropdown" class="relative inline-block">
<%= button_tag data: { action: "click->dropdown#toggle click@window->dropdown#hide" }, class: "btn btn-primary" do %>
Dropdown
<%= icon "chevron-down", classes: "w-3 h-3 ml-2" %>
<% end %>
<div
class="hidden transition transform origin-top-left absolute left-0 top-10 bg-white rounded-lg shadow-xl shadow-slate-900/10 border border-slate-200 md:w-[200px] w-full z-50 py-2 dark:bg-slate-700 dark:shadow-slate-900/50 dark:border-slate-500/60 md:text-sm text-base font-medium text-slate-600 dark:text-slate-200"
data-dropdown-target="menu"
data-transition-enter-from="opacity-0 scale-95"
data-transition-enter-to="opacity-100 scale-100"
data-transition-leave-from="opacity-100 scale-100"
data-transition-leave-to="opacity-0 scale-95"
>
<%= link_to "Bookmark", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300" %>
<%= link_to "Report", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300" %>
<%= link_to "Export", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300" %>
</div>
</div>
.relative.inline-block{"data-controller" => "dropdown"}
= button_tag data: { action: "click->dropdown#toggle click@window->dropdown#hide" }, class: "btn btn-primary" do
Dropdown
\#{icon "chevron-down", classes: "w-3 h-3 ml-2"}
.hidden.transition.transform.origin-top-left.absolute.left-0.top-10.bg-white.rounded-lg.shadow-xl.border.border-slate-200.w-full.z-50.py-2.dark:bg-slate-700.md:text-sm.text-base.font-medium.text-slate-600.dark:text-slate-200{class: "shadow-slate-900/10 md:w-[200px] dark:shadow-slate-900/50 dark:border-slate-500/60", "data-dropdown-target" => "menu", "data-transition-enter-from" => "opacity-0 scale-95", "data-transition-enter-to" => "opacity-100 scale-100", "data-transition-leave-from" => "opacity-100 scale-100", "data-transition-leave-to" => "opacity-0 scale-95"}
= link_to "Bookmark", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300"
= link_to "Report", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300"
= link_to "Export", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300"
Dropdowns
With menu icons
<div data-controller="dropdown" class="relative inline-block">
<button type="button" data-action="click->dropdown#toggle click@window->dropdown#hide" class="btn btn-primary pr-3">
Dropdown
<%= icon "chevron-down", classes: "w-3 h-3 ml-2" %>
</button>
<div
class="hidden transition transform origin-to-left absolute left-0 top-10 bg-white rounded-lg shadow-xl shadow-slate-900/10 border border-slate-200 md:w-[200px] w-full z-50 py-2 dark:bg-slate-700 dark:shadow-slate-900/50 dark:border-slate-500/60 md:text-sm text-base font-medium text-slate-600 dark:text-slate-200"
data-dropdown-target="menu"
data-transition-enter-from="opacity-0 scale-95"
data-transition-enter-to="opacity-100 scale-100"
data-transition-leave-from="opacity-100 scale-100"
data-transition-leave-to="opacity-0 scale-95">
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 dark:hover:text-indigo-300 group flex items-center justify-start space-x-3">
<%= icon "bookmark", classes: "w-4 h-4 text-slate-600 group-hover:text-indigo-600 flex-shrink-0" %>
<span>Bookmark</span>
</a>
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 dark:hover:text-indigo-300 group flex items-center justify-start space-x-3">
<%= icon "flag", classes: "w-4 h-4 text-slate-600 group-hover:text-indigo-600 flex-shrink-0" %>
<span>Report</span>
</a>
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 dark:hover:text-indigo-300 group flex items-center justify-start space-x-3">
<%= icon "arrow-down-tray", classes: "w-4 h-4 text-slate-600 group-hover:text-indigo-600 flex-shrink-0" %>
<span>Export</span>
</a>
</div>
</div>
<div data-controller="dropdown" class="relative inline-block">
<%= button_tag data: { action: "click->dropdown#toggle click@window->dropdown#hide" }, class: "btn btn-primary" do %>
Dropdown
<%= icon "chevron-down", classes: "w-3 h-3 ml-2" %>
<% end %>
<div
class="hidden transition transform origin-to-left absolute left-0 top-10 bg-white rounded-lg shadow-xl shadow-slate-900/10 border border-slate-200 md:w-[200px] w-full z-50 py-2 dark:bg-slate-700 dark:shadow-slate-900/50 dark:border-slate-500/60 md:text-sm text-base font-medium text-slate-600 dark:text-slate-200"
data-dropdown-target="menu"
data-transition-enter-from="opacity-0 scale-95"
data-transition-enter-to="opacity-100 scale-100"
data-transition-leave-from="opacity-100 scale-100"
data-transition-leave-to="opacity-0 scale-95">
<%= link_to, class: "px-4 py-[.4rem] hover:text-indigo-600 dark:hover:text-indigo-300 group flex items-center justify-start space-x-3" do %>
<%= icon "bookmark", classes: "w-4 h-4 text-slate-600 group-hover:text-indigo-600 flex-shrink-0" %>
<span>Bookmark</span>
<% end %>
<%= link_to, class: "px-4 py-[.4rem] hover:text-indigo-600 dark:hover:text-indigo-300 group flex items-center justify-start space-x-3" do %>
<%= icon "flag", classes: "w-4 h-4 text-slate-600 group-hover:text-indigo-600 flex-shrink-0" %>
<span>Report</span>
<% end %>
<%= link_to, class: "px-4 py-[.4rem] hover:text-indigo-600 dark:hover:text-indigo-300 group flex items-center justify-start space-x-3" do %>
<%= icon "arrow-down-tray", classes: "w-4 h-4 text-slate-600 group-hover:text-indigo-600 flex-shrink-0" %>
<span>Export</span>
<% end %>
</div>
</div>
.relative.inline-block{"data-controller" => "dropdown"}
= button_tag data: { action: "click->dropdown#toggle click@window->dropdown#hide" }, class: "btn btn-primary" do
Dropdown
\#{icon "chevron-down", classes: "w-3 h-3 ml-2"}
.hidden.transition.transform.origin-to-left.absolute.-left-1.top-10.bg-white.rounded-lg.shadow-xl.border.border-slate-200.z-50.py-2.dark:bg-slate-700.text-sm.font-medium.text-slate-600.dark:text-slate-200{class: "shadow-slate-900/10 min-w-[200px] dark:shadow-slate-900/50 dark:border-slate-500/60", "data-dropdown-target" => "menu", "data-transition-enter-from" => "opacity-0 scale-95", "data-transition-enter-to" => "opacity-100 scale-100", "data-transition-leave-from" => "opacity-100 scale-100", "data-transition-leave-to" => "opacity-0 scale-95"}
= link_to, class: "px-4 py-[.4rem] hover:text-indigo-600 dark:hover:text-indigo-300 group flex items-center justify-start space-x-3" do
= icon "bookmark", classes: "w-4 h-4 text-slate-600 group-hover:text-indigo-600 flex-shrink-0"
%span Bookmark
= link_to, class: "px-4 py-[.4rem] hover:text-indigo-600 dark:hover:text-indigo-300 group flex items-center justify-start space-x-3" do
= icon "flag", classes: "w-4 h-4 text-slate-600 group-hover:text-indigo-600 flex-shrink-0"
%span Report
= link_to, class: "px-4 py-[.4rem] hover:text-indigo-600 dark:hover:text-indigo-300 group flex items-center justify-start space-x-3" do
= icon "arrow-down-tray", classes: "w-4 h-4 text-slate-600 group-hover:text-indigo-600 flex-shrink-0"
%span Export
Dropdowns
With dividers
Divide items in a list using a divide-y class from Tailwind CSS.
Depending on how your dropdown list data originates, you may need to get creative on assigning padding between the first and last items in the dropdown menu.
By default each item has a padding offset of py-1 applied. You may find that you will need to override this for the first and last items to produce an even gap across all menu list items while displaying dividers.
<div data-controller="dropdown" class="relative inline-block">
<button type="button" data-action="click->dropdown#toggle click@window->dropdown#hide" class="btn btn-white pr-3">
Dropdown with dividers
<%= icon "chevron-down", classes: "w-3 h-3 ml-2" %>
</button>
<div
class="hidden transition transform origin-top-left absolute left-0 top-10 bg-white rounded-lg shadow-xl shadow-slate-900/10 border border-slate-200 md:w-[200px] w-full z-50 py-2 dark:bg-slate-700 dark:shadow-slate-900/50 dark:border-slate-500/60 md:text-sm text-base font-medium text-slate-600 dark:text-slate-200 divide-y dark:divide-slate-500/75"
data-dropdown-target="menu"
data-transition-enter-from="opacity-0 scale-95"
data-transition-enter-to="opacity-100 scale-100"
data-transition-leave-from="opacity-100 scale-100"
data-transition-leave-to="opacity-0 scale-95"
>
<div role="none" class="pb-1">
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 block">Edit</a>
</div>
<div role="none" class="py-1">
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 block">Share</a>
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 block">Export</a>
</div>
<div role="none" class="pt-1">
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 block">Delete</a>
</div>
</div>
</div>
<div data-controller="dropdown" class="relative inline-block">
<%= button_tag data: { action: "click->dropdown#toggle click@window->dropdown#hide" }, class: "btn btn-primary" do %>
Dropdown with dividers
<%= icon "chevron-down", classes: "w-3 h-3 ml-2" %>
<% end %>
<div
class="hidden transition transform origin-top-left absolute left-0 top-10 bg-white rounded-lg shadow-xl shadow-slate-900/10 border border-slate-200 md:w-[200px] w-full z-50 py-2 dark:bg-slate-700 dark:shadow-slate-900/50 dark:border-slate-500/60 md:text-sm text-base font-medium text-slate-600 dark:text-slate-200 divide-y dark:divide-slate-500/75"
data-dropdown-target="menu"
data-transition-enter-from="opacity-0 scale-95"
data-transition-enter-to="opacity-100 scale-100"
data-transition-leave-from="opacity-100 scale-100"
data-transition-leave-to="opacity-0 scale-95"
>
<div role="none" class="pb-1">
<%= link_to "Edit", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block" %>
</div>
<div role="none" class="py-1">
<%= link_to "Share", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block" %>
<%= link_to "Export", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block" %>
</div>
<div role="none" class="pt-1">
<%= link_to "Delete", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block" %>
</div>
</div>
</div>
.relative.inline-block{"data-controller" => "dropdown"}
= button_tag data: { action: "click->dropdown#toggle click@window->dropdown#hide" }, class: "btn btn-primary" do
Dropdown with dividers
#{icon "chevron-down", classes: "w-3 h-3 ml-2"}
.hidden.transition.transform.origin-top-left.absolute.left-0.top-10.bg-white.rounded-lg.shadow-xl.border.border-slate-200.w-full.z-50.py-2.dark:bg-slate-700.md:text-sm.text-base.font-medium.text-slate-600.dark:text-slate-200.divide-y{class: "shadow-slate-900/10 md:w-[200px] dark:shadow-slate-900/50 dark:border-slate-500/60 dark:divide-slate-500/75", "data-dropdown-target" => "menu", "data-transition-enter-from" => "opacity-0 scale-95", "data-transition-enter-to" => "opacity-100 scale-100", "data-transition-leave-from" => "opacity-100 scale-100", "data-transition-leave-to" => "opacity-0 scale-95"}
.pb-1{role: "none"}
= link_to "Edit", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block"
.py-1{role: "none"}
= link_to "Share", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block"
= link_to "Export", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block"
.pt-1{role: "none"}
= link_to "Delete", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block"
Dropdowns
Right aligned menu
<div data-controller="dropdown" class="relative inline-block">
<button type="button" data-action="click->dropdown#toggle click@window->dropdown#hide" class="btn btn-white pr-3">
Right-aligned Dropdown Menu
<%= icon "chevron-down", classes: "w-3 h-3 ml-2" %>
</button>
<ul
class="hidden transition transform origin-top-right absolute -left-1 top-10 bg-white rounded-lg shadow-xl shadow-slate-900/10 border border-slate-200 min-w-[200px] z-50 py-2 dark:bg-slate-700 dark:shadow-slate-900/50 dark:border-slate-500/60 text-sm font-medium text-slate-600 dark:text-slate-200"
data-dropdown-target="menu"
data-transition-enter-from="opacity-0 scale-95"
data-transition-enter-to="opacity-100 scale-100"
data-transition-leave-from="opacity-100 scale-100"
data-transition-leave-to="opacity-0 scale-95"
>
<li>
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300">Bookmark</a>
</li>
<li>
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300">Report</a>
</li>
<li>
<a href="#" class="px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300">Export</a>
</li>
</ul>
</div>
<div data-controller="dropdown" class="relative inline-block">
<%= button_tag data: { action: "click->dropdown#toggle click@window->dropdown#hide" }, class: "btn btn-primary" do %>
Right-aligned Dropdown Menu
<%= icon "chevron-down", classes: "w-3 h-3 ml-2" %>
<% end %>
<div
class="hidden transition transform origin-top-right absolute -left-1 top-10 bg-white rounded-lg shadow-xl shadow-slate-900/10 border border-slate-200 min-w-[200px] z-50 py-2 dark:bg-slate-700 dark:shadow-slate-900/50 dark:border-slate-500/60 text-sm font-medium text-slate-600 dark:text-slate-200"
data-dropdown-target="menu"
data-transition-enter-from="opacity-0 scale-95"
data-transition-enter-to="opacity-100 scale-100"
data-transition-leave-from="opacity-100 scale-100"
data-transition-leave-to="opacity-0 scale-95"
>
<div>
<%= link_to "Bookmark", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300" %>
</div>
<div>
<%= link_to "Report", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300" %>
</div>
<div>
<%= link_to "Export", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300" %>
</div>
</div>
</div>
.relative.inline-block{"data-controller" => "dropdown"}
= button_tag data: { action: "click->dropdown#toggle click@window->dropdown#hide" }, class: "btn btn-primary"
Right-aligned Dropdown Menu
= icon "chevron-down", classes: "w-3 h-3 ml-2"
.hidden.transition.transform.origin-top-right.absolute.-left-1.top-10.bg-white.rounded-lg.shadow-xl.border.border-slate-200.z-50.py-2.dark:bg-slate-700.text-sm.font-medium.text-slate-600.dark:text-slate-200{class: "shadow-slate-900/10 min-w-[200px] dark:shadow-slate-900/50 dark:border-slate-500/60", "data-dropdown-target" => "menu", "data-transition-enter-from" => "opacity-0 scale-95", "data-transition-enter-to" => "opacity-100 scale-100", "data-transition-leave-from" => "opacity-100 scale-100", "data-transition-leave-to" => "opacity-0 scale-95"}
%div
= link_to "Bookmark", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300"
%div
= link_to "Report", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300"
%div
= link_to "Export", "#", class: "px-4 py-[.4rem] hover:text-indigo-600 block dark:hover:text-indigo-300"