Skip to content

Blogging On Rails

Everything on Rails!

Headless UI with StimulusJS and an Outlet

Headless UI 2.0 just came out from TailwindLabs. I often find myself using their components in my projects, and I wanted to show you my process for converting their React components to use Stimulus, since I haven’t been using React in any of my projects.

Switching on Interactivity

The component I wanted to use was the Switch. It’s styled after the native iOS switch toggle, where you turn an option on or off. Tapping anywhere on the button will transition the inner circle from one side to the other. The version of this switch uses CSS animations, and all our controller would need to do is update a data-checked attribute on the button, and CSS will animate the sliding action.

First, I copied the HTML from the rendered React component, and you get:

<button 
    class="group relative flex h-7 w-14 cursor-pointer rounded-full bg-blue-900/10 p-1 transition-colors duration-200 ease-in-out focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white data-[checked]:bg-blue-900/30" 
    role="switch" 
    type="button" 
    tabindex="0" 
    aria-checked="true"
	data-checked>
    <span aria-hidden="true" class="pointer-events-none inline-block size-5 translate-x-0 rounded-full bg-white ring-0 shadow-lg transition duration-200 ease-in-out group-data-[checked]:translate-x-7"></span>
  </button>

Let’s generate a Stimulus controller called switch that will handle the clicks for us:

$ ./bin/rails g stimulus switch 

Add the controller and an action to the <button> tag:

data-controller="switch"
data-action="switch#toggle"

Then when the button is clicked, it will fire the toggle action on the controller:

  toggle() {
    if (this.element.dataset.checked) {
      delete this.element.dataset.checked;
      this.element.ariaChecked = false;
    } else {
      this.element.dataset.checked = true;
      this.element.ariaChecked = true;
    }
  }

Now when you click the button, the switch toggles back and forth.

Adding an Outlet for the Switch

This switch just exists on its own right now. It would be great if it could actually update a value on a form. We can fix this with Stimulus Outlets. This ability allows our controller to talk to another controller on the page. Stimulus uses the controller name and a CSS path to connect to the correct element on the page. We start by adding the input checkbox that we want to access:

<input type="checkbox" id="todo-complete" data-controller="input">

Then we add a data attribute to the switch controller:

data-switch-input-outlet="#todo-complete"

In the Stimulus controller, we add the outlet declaration:

static outlets = ["input"];

Now, we can access the inputOutletElement in the switch controller, and set it to checked, and read its value. You can update the toggle() method:

  toggle() {
    this.inputOutletElement.checked = !this.inputOutletElement.checked;
    if (this.inputOutletElement.checked) {
      this.element.dataset.checked = true;
      this.element.ariaChecked = true;
    } else {
      delete this.element.dataset.checked;
      this.element.ariaChecked = false;
    }
  }

In Closing

Headless UI is great for some prebuilt components using Tailwind CSS. Even though they’re built in React, we can use Stimulus to add some interactivity. Stimulus Outlets allow us to target elements on the page outside the hierarchy of the controller. I can’t wait to use this more in my applications.

2 comments on “Headless UI with StimulusJS and an Outlet”

  1. Thanks for sharing this. Love Tailwind and always felt it’s a shame their Headless UI components are available to us non-React users.

    If you convert any more components, please consider open sourcing them.

Leave a Reply

Your email address will not be published. Required fields are marked *