DEV Community

Cover image for Simple dropdown with tailwindcss and stimulus
Ana Nunes da Silva
Ana Nunes da Silva

Posted on • Updated on • Originally published at ananunesdasilva.com

Simple dropdown with tailwindcss and stimulus

I'm surprised how easy it is to create UI components with tailwindcss and stimulus.

1. Install or upgrade tailwindcss (v2.0)

Tailwindcss v2.0 depends on PostCSS 8. Currently, Webpack only supports PostCSS 7 so we will have to install a compatibility build:

yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
Enter fullscreen mode Exit fullscreen mode

Tailwind assures that we're not missing on any features but once Webpack adds support to PostCSS 8 we can re-install Tailwind and its peer dependencies using the latest tag:

yarn uninstall tailwindcss @tailwindcss/postcss7-compat
yarn install tailwindcss@latest postcss@latest autoprefixer@latest
Enter fullscreen mode Exit fullscreen mode

2. Install stimulus in your rails app (if you haven't already)

bundle exec rails webpacker:install:stimulus
Enter fullscreen mode Exit fullscreen mode

This will create two files, an index.js that loads all stimulus controllers and an example of a controller under home_controller.js.

app/javascript/controllers/index.js:

// Load all the controllers within this directory and all subdirectories.   
// Controller files must be named *_controller.js.                          

import { Application } from "stimulus"                                      
import { definitionsFromContext } from "stimulus/webpack-helpers"           

const application = Application.start()                                     
const context = require.context("controllers", true, /_controller\.js$/)    
application.load(definitionsFromContext(context))
Enter fullscreen mode Exit fullscreen mode

app/javascript/controllers/home_controller.js

 // Visit The Stimulus Handbook for more details                         
 // <https://stimulusjs.org/handbook/introduction>                         
 //                                                                      
 // This example controller works with specially annotated HTML like:    
 //                                                                      
 // <div data-controller="hello">                                        
 //   <h1 data-target="hello.output"></h1>                               
 // </div>                                                               

 import { Controller } from "stimulus"                                   

 export default class extends Controller {                               
   static targets = [ "output" ]                                         

   connect() {                                                           
     this.outputTarget.textContent = 'Hello, Stimulus!'                  
   }                                                                     
 }
Enter fullscreen mode Exit fullscreen mode

I'm going to rename home_controller.js to menu_controller.js and use it as a specific controller that will handle all navigation functionalities. For now I will only add the dropdown toggle logic but in the future I might add more (e.g. dark mode functionality).

That easy, ready to go!

3. Add the button and dropdown HTML

In my case, I'm going to add this button to my navbar.

Tailwindcss has great free components we can use so I'm going to copy the code of the simple dropdown available here.

I only want to show this button on small screens so I changed the button's svg to a hamburger - that I copied and adjusted from css tricks - and I also added a tailwindcss lg:hidden class to hide the button on large screens.

The default state of the dropdown should be hidden but at this point it is always showing. We can add tailwind's hidden class to solve that. Still, if you click on the button nothing happens yet. The dropdown does not show because we need javascript for that. This is where stimulus comes in.

At this point, this is the component's html:

<%# Adding lg:hidden here to hide the button on large screens %> 
<div class="relative inline-block pt-10 text-left lg:hidden">   

   <%# Hamburger button %>                                                                                                                        
   <div>                                                                                                                                                                        
     <button type="button" class="inline-flex justify-center w-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500" 
       id="options-menu" aria-haspopup="true" aria-expanded="true">                                                                                                                 
       <svg viewBox="0 0 100 80" width="60" height="60">                                                                                                                            
           <rect width="100" height="20" rx="8"></rect>                                                                                                                             
           <rect y="30" width="100" height="20" rx="8"></rect>                                                                                                                      
           <rect y="60" width="100" height="20" rx="8"></rect>                                                                                                                      
       </svg>                                                                                                                                                                       
     </button>                                                                                                                                                                      
   </div>                                                                                                                                                                           

   <%# Dropdown menu with a default hidden class %>                                                                                                                                                                        
   <div class="hidden absolute right-0 w-56 mt-2 bg-white shadow-lg origin-top-right rounded-md ring-1 ring-black ring-opacity-5">                                                         
     <div class="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">                                                                                      
       <a href="/about" class="block px-4 py-2 text-4xl text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">About</a>                                              
       <a href="/posts" class="block px-4 py-2 text-4xl text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">Posts</a>                                              
     </div>                                                                                                                                                                         
   </div>                                                                                                                                                                           
 </div>
Enter fullscreen mode Exit fullscreen mode

4. Add the toggle functionality

First we need to connect the menu controller to the component. Stimulus does that using a data-controller attribute that accepts the name of the controller as a value:

This should be placed in the component's opening div.

<div data-controller="menu" class="relative inline-block pt-10 text-center lg:hidden">
Enter fullscreen mode Exit fullscreen mode

Now, I will have to make the dropdown element available to the controller and for that stimulus uses a data-[identifier]-target attribute, where the identifier is the name of the controller.

Note: If you are using stimulus 1.0 the attribute is data-target and not data-[identifier]-target. Version 2.0 still accepts data-target but be aware that this will be deprecated.

I want to target the dropdown element so I'll add this attribute to its opening div and I'll give it a toggleable value (as in something that can be toggled).

<div data-menu-target="toggleable" class="absolute right-0 hidden mt-2 bg-white shadow-lg w-96 origin-top-right rounded-md ring-1 ring-black ring-opacity-5">
Enter fullscreen mode Exit fullscreen mode

For this to work, this toggleable target will need to be registered in the controller:

import { Controller } from "stimulus"                  

 export default class extends Controller {              
   static targets = [ "toggleable" ]                    

   // toggle function will be here                                                    
 }
Enter fullscreen mode Exit fullscreen mode

Now that we have the target, we need to add a click event to the hamburger button. This is the event that will trigger the toggle function that in turn will use the registered target to change the css.

Stimulus uses a data-action attribute that accepts an event type followed by the function we want to trigger. So, on the button element we will add:

 <div data-action="click->menu#toggle">   
Enter fullscreen mode Exit fullscreen mode

Have a look at the final HTML where I've commented above the elements where I placed these three attributes.

<%# data-controller with the name of the controller that will listen to the events on this element %>                                                                              
  <div data-controller="menu" class="relative inline-block pt-10 text-center lg:hidden">                                                                                             

<%# data-action with the event type that will trigger a toggle function on the menu controller %>                                                                                  
    <div data-action="click->menu#toggle">                                                                                                                                           
      <button type="button" class="inline-flex justify-center w-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500" 
        id="options-menu" aria-haspopup="true" aria-expanded="true">                                                                                                                 
        <svg viewBox="0 0 100 80" width="60" height="60">                                                                                                                            
            <rect width="100" height="20" rx="8"></rect>                                                                                                                             
            <rect y="30" width="100" height="20" rx="8"></rect>                                                                                                                      
            <rect y="60" width="100" height="20" rx="8"></rect>                                                                                                                      
        </svg>                                                                                                                                                                       
      </button>                                                                                                                                                                      
    </div>                                                                                                                                                                           

  <%# data-target - tells the controller which elements it should target %>                                                                                                                         
    <div data-menu-target="toggleable" class="absolute right-0 hidden mt-2 bg-white shadow-lg w-96 origin-top-right rounded-md ring-1 ring-black ring-opacity-5">                    
      <div class="py-6" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">                                                                                      
        <a href="/about" class="block px-4 py-5 text-5xl text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">About</a>                                              
        <a href="/posts" class="block px-4 py-5 text-5xl text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">Posts</a>                                              
      </div>                                                                                                                                                                         
    </div>                                                                                                                                                                           
  </div>
Enter fullscreen mode Exit fullscreen mode

We've added the toggle function to the HTML and now we need to go the controller to define it and add its logic.

First thing you can do is just log something to the console to make sure that the previous html setup is working.

import { Controller } from "stimulus"       

 export default class extends Controller {   
   static targets = [ "toggleable" ]         

   toggle() {
        console.log('it works')
     }        
 }
Enter fullscreen mode Exit fullscreen mode

Or you can also log the target element. If it's working properly you should see the dropdown HTML on your browser's console.

import { Controller } from "stimulus"       

 export default class extends Controller {   
   static targets = [ "toggleable" ]         

   toggle() {
        console.log(this.toggleableTarget)
     }        
 }
Enter fullscreen mode Exit fullscreen mode

The final step will be to make menu#toggle call the javascript classList toggle function that will add/remove the hidden class to/from the toggleable target (our dropdown element).

import { Controller } from "stimulus"       

 export default class extends Controller {   
   static targets = [ "toggleable" ]         

   toggle() {
         this.toggleableTarget.classList.toggle('hidden')
     }        
 }
Enter fullscreen mode Exit fullscreen mode

That's it! Check how it toggles! ๐ŸŽ‰

As I'm exploring tailwindcss and stimulus I'll be posting about here, on my website and on twitter. I also write about other rails and web development topics, so feel free to follow and DM me.

Top comments (2)

Collapse
 
cescquintero profile image
Francisco Quintero ๐Ÿ‡จ๐Ÿ‡ด

Aah, thanks. I'm studying Stimulus and was struggling with some minor details and this was really helpful.

Thanks for sharing, Ana โœŒ๐Ÿฝ

Collapse
 
anakbns profile image
Ana Nunes da Silva

So glad it helped! Thanks for your feedback!