Write better ViewComponents with Tailwind CSS and Hotwire
Tags: rails, hotwire, tailwincss, viewcomponent
Published: 2024-01-22 04:07:54

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](https://hotwired.dev/), [Tailwind CSS](https://tailwindcss.com/) and [ViewComponent](https://viewcomponent.org/).

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](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-token_list), it's nothing more than an alias for `token_list()`.

Let's look at an example (taking from the [Rails Designer dropdown component](https://railsdesigner.com/)).

```rb
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?
  }
)
```

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:

```css
"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](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag). So you could use it like this too:

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

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

## 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`](https://tailwindcss.com/docs/functions-and-directives#apply), I use in all my Rails apps.

```css
@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:

```erb
link_to "Rails Designer`, "http://railsdesigner.com/"
```

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

```erb
link_to "Rails Designer`, "http://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](https://railsdesigner.com/) Navbar component.

```html
<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](https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state) 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:

```js
  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 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:

```erb
    <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 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:

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

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

```js
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.


## 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:

```rb
# 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:

```rb
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.


## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kesog2ggnalmb7viiils.jpg)

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):

```html
…
<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.

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!