Quicktips for ViewComponent with Tailwind CSS/Hotwire

A 3d rendering of a cute, but cool turtle on a skateboard cruising.

Front-end code has historically been looked down upon a bit. “HTML is not a real language!”, ”CSS sucks!!” ”…and so does JavaScript!!1!”. Which is a shame. Rails gives one-person development teams a true super power, allowing them to build a complete, successful product from 0 to customers.

Next to Rails there are other first- and third-party tools to elevate the joy of writing Rails apps even more. Amongst those: Hotwire, Tailwind CSS and ViewComponent.

I’ve collected some tips & tricks (and maybe best practices) how I use these tools in my Rails SaaS apps all the time. Over time I might update this article if I encounter another one (that I forgot when writing this).

Use class_names() to (conditionally) build a list of CSS selectors

class_names() is a constant go-to method for me due to its straightforward approach to:

  1. conditionally add/remove CSS selectors;
  2. keep a (long) list of Tailwind CSS utility classes manageable.

As you can see in the documentation, it’s nothing more than an alias for token_list().

Let’s look at an example (taken from the Rails Designer DropdownComponent).

class_names(
  "absolute text-sm shadow-xl overflow-hidden rounded-lg z-10",
  content_min_max_width,
  @padding,
  {
    "bg-white/80 ring-1 ring-inset ring-gray-100 backdrop-blur-md": @theme.light?,
    "bg-gray-800": @theme.light?
  }
)

The first lines shows some “static“ Tailwind CSS utility classes (absolute…z-10). Those will always be applied. Then some selectors set in a method called content_min_max_width—you can imagine some extra work is done in this method. Then something is added coming from the instance variable @padding—this could be an attribute added to the component. And then within the curly braces ({}) are selectors set based on if @theme.light? or @theme.dark? return true.

The list of selectors that is built with the defaults for the DropdownComponent then would be:

"absolute text-sm shadow-xl overflow-hidden rounded-lg z-10 min-w-[8rem] max-w-[16rem] p-4 bg-white/80 ring-1 ring-inset ring-gray-100 backdrop-blur-md"

The class_names() method is also “built-in” in the tag-method. So you could use it like this too:

tag("div", class: { "block": Current.user.admin?, "hidden": !Current.user.admin? })

# => <div class="highlight" />

CSS has over the years become much more powerful. Gone are the days of just selecting by class (.class) or id (#unique_id). You can also select other attributes to target HTML elements. Like the following CSS, using Tailwind CSS’s @apply, I use in all the time.

@layer base {
  /* … */
  a:not([class]) {
    @apply underline;

    &:hover {
      @apply no-underline;
    }
  }
  /* … */
}

This will add an underline to any link_to (and no-underline on hover) without the class-attribute, like this one:

link_to "Rails Designer`, "https://railsdesigner.com/"

But when you add even a blank class: "" the default styles will not be applied.

link_to "Rails Designer`, "https://railsdesigner.com/", class: ""

You can of course tweak how you want links to be styled.

Use data-attributes in CSS to toggle visibility

If you’ve been around for some time, you probably seen hacks like prefixing CSS-classes with js- to mark these are used somehow and somewhere in JavaScript.

Just as the previous tip, you can use data-attributes to cascade down the element too. Let’s check out an example again from the Rails Designer NavbarComponent.

<nav class="group/navigation">
  <ul class="hidden group-data-[show-menu]/navigation:block">
  </ul>
</nav>

The trick here is to use the group modifier that Tailwind CSS provides.

This hides the ul-element by default (with hidden), but once the nav-element has the data-show-menu attribute, the ul-element is shown. You can imagine this needs a really simple, reusable stimulus controller to toggle such an attribute.

Style components differently in (or outside) a turbo-frame

I find turbo-frames a great way to show modals (or dialogs). Upon clicking a link the page is rendered within the turbo_frame. But if this is an external page, that should be able to be displayed standalone too, you likely want to remove some styling, like a drop-shadow or some (absolute or fixed) positioning.

Fortunately, setting this up with Tailwind CSS is a straightforward process. Within your tailwindcss.config.js, add the following to the plugins array:

  plugins: [
    // …
    function ({ addVariant }) {
      addVariant("turbo-frame", "turbo-frame[src] &")
    }
  ]

You now have a custom modifier turbo-frame. It works exactly like other Tailwind CSS modifiers too, for example target breakpoints (md: and lg:).

You can now create a modal component that only has a drop-shadow when rendered inside a turbo-frame. Like this:

<div class="relative turbo-frame:shadow-xl"></div>

The selector turbo-frame[src] & works the same as seen in the previous tips.

Use private functions in your Stimulus controller

Ruby can have private (and protected) methods. They are created, amongst other options, simply by moving the method below the private (Kernel) method.

And just like it is good practice to keep public methods minimal in your Ruby classes, it is also, for the same reasons, a good idea to stick to as few public functions in your Stimulus/JavaScript classes.

How to create a private function in Javascript? By prefixing it with a #.

Like so:

class Class {
  #thisIsPrivate() {
    //...
  }
}

A typical Stimulus controller that I write could look like this:

export default class extends Controller {
  initialize() {
  }

  connect() {
  }

  disconnect() {
  }

  // actions defined here

  // private

  #firstPrivateFunction() {
  }

  #secondPrivateFunction() {
  }
}

The default connect and disconnect come at the top. Followed by the actions that are available in the Stimulus controller. Then after the // private comment (that does precisely nothing), come all private functions.

The // private comment is only there for a visual cue. I am using exactly this style, because it is similar the one in Ruby classes.

This helps me organize the controllers and keep things easier to reason about.

Need more ideas? Check out this article on writing proper Stimulus controllers.

Ensure turbo_frame request

If you ever built a UI that relied on modals a fair bit you might know how it can be annoying to style. Click a link, modal opens, make tweaks, refresh, click the link again, etc.

Fortunately, with Rails you can build the modal as a standalone page (eg. “automations/new”), and then tweak the style as needed. But this has the potential downside of your users viewing this page standalone. Maybe that doesn’t work for your app.

That’s why I have this controller concern in all my Rails apps:

# app/controllers/concerns/frameable.rb

module Frameable
  extend ActiveSupport::Concern

  private

  def ensure_turbo_frame_response
    redirect_to root_path unless turbo_frame_request?
  end

  def production_environment?
    Rails.env.production?
  end
end

Then for any action that should only be visible within a turbo-frame I use it like this:

before_action :ensure_turbo_frame_response, only: %w[new], if: :production_environment?

This makes sure the page can only be viewed within a turbo-frame, but only in the production environment.

Set a delay on hover transitions

This is a tiny UX tip, but will spark joy! When you have transitions added to elements, like a card. Add a short delay, so when the user moves the cursor it doesn’t trigger all kinds of (aborted) transitions. Keeping things a bit more in check.

Like so (using Tailwind CSS classes):

<li class="flex px-4 py-2 bg-white transition ease-in-out duration-200 delay-75 hover:bg-gray-50"></li>

While quickly hovering any of these li-elements the background will now not change, but only after 75ms. Making sure the user doesn’t get distracted by unwanted transitions.

Check these design tips using Tailwind CSS for more.

These are some of the tips and ideas I use in all my Rails apps. Have a favorite? Or one that is missing? Share them!

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built using ViewComponent

  • Designed with Tailwind CSS

  • Enhanced with Hotwire