Write better ViewComponents with Tailwind CSS and Hotwire

railsdesigner

Rails Designer

Posted on January 22, 2024

Write better ViewComponents with Tailwind CSS and Hotwire

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 A to Z.

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 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 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 (taking from the Rails Designer dropdown component).

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": light_theme?,
    "bg-gray-800": dark_theme?
  }
)
Enter fullscreen mode Exit fullscreen mode

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 here. Then something is added coming from the instance variable @padding—this could be attribute added to the component. And then within the curly braces ({}) are selectors set based on if either light_theme? or dark_theme? return true.

The list of selectors that is built with the defaults for the Dropdown component 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"
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

Default styles for link_to (a-tags)

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 my Rails apps.

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

    &:hover {
      @apply no-underline;
    }
  }
  /* … */
}
Enter fullscreen mode Exit fullscreen mode

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`, "http://railsdesigner.com/"
Enter fullscreen mode Exit fullscreen mode

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

link_to "Rails Designer`, "http://railsdesigner.com/", class: ""
Enter fullscreen mode Exit fullscreen mode

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 Navbar component.

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

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 really needs a 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 function:

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

You now have a custom modifier turbo-frame. It works exactly like other Tailwind CSS modifiers to, 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>
Enter fullscreen mode Exit fullscreen mode

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 try to keep to one public method 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 functions.

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

Like so:

class Class {
  #thisIsPrivate() {
    //...
  }
}
Enter fullscreen mode Exit fullscreen mode

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() {
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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

Use opacity on colored backgrounds

This is a tiny UI tip that will get you a high-five from your designer if you add it without them asking. Guaranteed!

The idea is to use bg-green-200/50 instead of bg-green-100 (or any other Tailwind CSS color). This works specifically great if the parent element gets a gray background (on hover).

Check this image:

Before and after preview of using opacity on colored backgrounds

It might be a bit hard to spot for the untrained eye, but the green badge in the bottom left (without opacity) looks a tad bit muddier than the one on the bottom-right (with opacity). Give it a try!

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>
Enter fullscreen mode Exit fullscreen mode

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.

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

💖 💪 🙅 🚩
railsdesigner
Rails Designer

Posted on January 22, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related