Styling Simple Form forms with Tailwind
Tags: tailwindcss, rails, simpleform, webdev
Published: 2022-06-20 17:24:30

During our [recent redesign](https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o) of the admin section of our web, we wanted to set up a system that would allow us to **quickly build good looking forms**. We like to use the **[Simple Form](https://github.com/heartcombo/simple_form) gem** for our forms, we have been using it for nearly 10 years, and we still appreciate its very succinct but flexible syntax. Also, we style our web with [Tailwind](https://tailwindcss.com/) and we definitely wanted to leverage it for the new admin pages, too. So, at one point of the redesign effort we had to deal with the **”How do we style our new admin forms with Tailwind?“** task and we wanted to do it in the most reusable way possible and without losing Simple Form's flexibility.

In our [previous post](https://dev.to/nejremeslnici/from-html-to-simple-form-anatomy-of-rails-forms-19m6) we ran through the basics of Rails forms rendering and how custom Rails form builders work. In this post, we will build on that remembering that **Simple Form is basically just a custom Rails form builder**. We will try to briefly describe what components it uses to render the forms and how to amend them to enable flexible Tailwind styling.

## Does Simple Form support Tailwind?

Well, yes and no. Simple Form has no Tailwind-specific configuration baked in currently – the details are discussed in [this GitHub issue](https://github.com/heartcombo/simple_form/issues/1723). However, **Simple Form is styling system-agnostic** so nothing should stop us if we sprinkled the Simple Form [configuration](https://github.com/heartcombo/simple_form#configuration) with our set of Tailwind classes for each of the form components. (A [”component“](https://github.com/heartcombo/simple_form#custom-components), in Simple Form jargon, is an input, its label, a hint and error message by default and we can define a custom component for anything else if we like.). 

And indeed, for simple scenarios, this should work well. Problems arise when we need to **divert from the default look** for a specific field. To understand that, we need to talk a bit about how Simple Form combines the default classes from the configuration with the custom classes that are meant to override them. For each component (input, label, etc.), Simple Form recognizes a **`<component>_html` hash of options**. CSS classes are handled via the `:class` key of this hash. So, for example, `label_html: { class: "custom-class" }` represents a way to tell Simple Form that we want a specific input’s label to have the `"custom-class"` class.

Now, how does this `"custom-class"` get merged with the default classes that we may have defined for the labels? The answer is that Simple Form simply **appends the custom class(es) to the default classes**. This may work well for traditional CSS styling where we can easily target arbitrary combinations of elements, ids and classes in the CSS and thus ensure that the [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) of the `"custom-class"` will be high enough to override the defaults. In Tailwind, most classes have the same specificity and in that situation, according to the [CSS cascading rules](https://developer.mozilla.org/en-US/docs/Web/CSS/Cascade#cascading_order), **the class that appears _last_ in the Tailwind-generated CSS file is the one that wins** which is most probably not what we want.

To demonstrate this problem with an example, let’s say we have the following default styles defined for a label in the Simple Form configuration:

```ruby
# config/initializers/simple_form.rb
config.wrappers :default do |b|
  ...
  b.use :label, class: "font-medium"
  ...
end
```

This config sets a ”medium“ font weight for our form labels by default. Now, suppose we want a specific input’s label to be bold instead, we might want to try the following naive approach (we’re using the [Slim template](http://slim-lang.com/) notation here):

```slim
/ some_template.html.slim
= simple_form_for(@user) do |f|
  = f.input :e_mail, label: "Important e-mail", \
                     label_html: { class: "font-bold" }
```

When we inspect the output HTML, everything seems to be in order – the `"font-bold"` is properly added to the set of classes generated for the label and to the default class `"font-medium"`:

![Generated HTML for our sample form showing the CSS cascade problem](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g2uev50nnric5kpjv0xk.png)
 
But the label is still not rendered bold! The explanation is that the `"font-medium"` class **happens to be listed below** the `"font-bold"` class in the CSS file generated by Tailwind so, according to the CSS cascade rules, it wins:

![The CSS cascade problem](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ap0qwb3ikw5z9ry10caw.png)
 
When trying to override default Tailwind classes in Simple Form, we need to be able to **not only _add_ new classes but also _remove_** some of the default ones. This is not something that Simple Form supports out of the box so we must help it a bit. 

One option is the [simple_form_tailwind_css gem](https://github.com/abevoelker/simple_form_tailwind_css) by Abe Voelker, [mentioned](https://github.com/heartcombo/simple_form/issues/1723#issuecomment-780303125) also in the GitHub issue. It uses a combination of default Tailwind styles in the [Simple Form configuration](https://github.com/abevoelker/simple_form_tailwind_css/blob/master/lib/generators/simple_form/tailwind/templates/simple_form.rb) with a [custom form builder](https://github.com/abevoelker/simple_form_tailwind_css/blob/master/lib/simple_form/tailwind/form_builder.rb) and a few [custom inputs](https://github.com/abevoelker/simple_form_tailwind_css/tree/master/lib/simple_form/tailwind/inputs). The gem supports replacing the default classes related to error messages with custom ones.

This gem was a great source of inspiration for us but we wanted to add a bit more flexibility to the process of overriding default Tailwind classes so we chose our own custom solution that we’ll further describe below.

## Our solution to the problem

Let’s first recap the goals that we wanted to achieve with this solution:

- we wanted great looking, Tailwind-styled Simple Form forms on our new admin pages by default,
- but with the option to amend the style of a particular field or its part if needed,
- from a technical point of view, we wanted the default form style and structure to be defined in a single place in the code base,
- and we preferred to be able to replace default classes selectively rather than all of them in bulk.

For the visual design and layout of the forms, we chose **[Tailwind UI](https://tailwindui.com/)**, a beautifully crafted set of Tailwind-styled components (more on that in a [previous post](https://dev.to/nejremeslnici/from-partials-to-viewcomponents-writing-reusable-front-end-code-in-rails-1c9o#the-design) if you like). Also, inspired by Tailwind UI, we decided to layout our new admin forms on a 12-column grid. That was the relatively easy part.

### Coming to a default class overriding API

To handle flexible overriding of the class methods, we explored several options that Simple Form would allow. First, we looked into the conventional way – defining default classes for all form parts in the Simple Form initialization file but in the end overriding them didn’t seem possible without monkey-patching Simple Form or re-defining all input types as custom inputs.

What we wanted to achieve here, was to be able to **override the defaults selectively**, i.e. we wanted to list the particular classes to remove from the defaults and add the ones that should override them. See the following sample usage for an overridden label style:

```slim
= simple_form_for(@user) do |f|
  = f.input :e_mail, label: "Important e-mail", \
                     label_html: { remove_default_class: "font-medium", \
                                   class: "font-bold" }
```

So, by specifying the **`:remove_default_class`** key in the options hash we wanted to selectively remove the named default classes and then the ”standard“ `:class` key would add the overriding classes. Thus, **this API would allow flexible and selective replacement of the form defaults**.

In the end, we decided to divert a bit from Simple Form conventions and build a **combination of a style-less wrapper configuration and a custom form builder**. Let’s show you how.

### The style-less wrapper configuration

We decided to put all class defaults **not** in the Simple Form configuration but into a custom form builder. The configuration then defined a ”wrapper“, i.e. a form layout structure, called `:plain`, that was intentionally class-less. The main part of it looks like this:

```ruby
  # config/initializers/simple_form.rb
  config.wrappers :plain do |b|
    ...

    b.use :label
    b.wrapper :input_wrapper, tag: :div do |component|
      component.use :input
    end
    b.use :hint,  wrap_with: { tag: :p }
    b.use :error, wrap_with: { tag: :p }
  end
```

This wrapper can render the following raw form structure:

```html
<form>
 <!-- for each field: -->
 <div> <!-- wrapper -->
   <label>...</label>  <!-- optional label -->
   <div> <!-- input wrapper -->
     <input .../>  <!-- the input itself -->
     <p>...</p>  <!-- optional hint -->
     <p>...</p>  <!-- optional error message -->
   </div>
 <div>
 ...
</form>
```

A few notes about this structure:

- the input is wrapped in a div: this is not essential but it makes it easier to add margins or set a flexbox layout for certain types of inputs (namely booleans),
- all other form components are as basic as it gets, no styling, no wrappers,
- this configuration allows the raw form layout to be potentially reused for form variants that should look different – we would just have to create a custom form builder for each form type.

### The custom form builder

Next, we built a [custom form builder](https://github.com/heartcombo/simple_form#custom-form-builder) that inherits from the [default Simple Form builder](https://github.com/heartcombo/simple_form/blob/main/lib/simple_form/form_builder.rb) and that takes care of both defining and overriding the default classes. (Have a look at our [previous post](https://dev.to/nejremeslnici/from-html-to-simple-form-anatomy-of-rails-forms-19m6#rails-form-builders) if you’re not sure what a Rails form builder is.)

In the custom builder, we re-defined the most important methods to build various types of form components, such as the `input` or `label` methods. The methods basically do just a couple of things:

- they define the default Tailwind classes for the given component (with a bit of programmatic logic applied, if needed),
- they allow to amend the defaults,
- and finally they pass the handling to the parent method via `super`.

Let’s show an example for a `label` method in the custom builder:

```ruby
class Builders::AdminFormBuilder < SimpleForm::FormBuilder
  def label(attribute_name, *args, &block)
    options = args.extract_options!.dup

    default_class = "block text-sm font-medium text-gray-700"
    options = arguments_with_updated_default_class(default_class, **options)

    super(attribute_name, *[args.first, options], &block)
  end
end
```

It works as follows:

- the `label` method signature is [the same](https://github.com/heartcombo/simple_form/blob/main/lib/simple_form/form_builder.rb#L324) as the one in the default Simple Form builder (we’re only using a named `block` variable instead of an implicit block),
- the method extracts the options that are passed to a field via the `label_html` options hash,
- it defines the default Tailwind classes for our form labels, including the `"font-medium"` class,
- it calls the `arguments_with_updated_default_class` method to update the hash with overridden classes (more on that below),
- and it reconstructs the original arguments and passes them to the parent method.

The **`arguments_with_updated_default_class` method** is a private method in the builder. It takes the default classes string and a hash of options, such as the `label_html` hash. From it it removes the classes named in the `:remove_default_class` key of the options hash and adds the new classes listed in the `:class` key:

```ruby
def arguments_with_updated_default_class(default_class, **kwargs)
  kwargs = kwargs.dup
  classes = default_class.to_s

  remove_key = :remove_default_class
  class_key = :class

  if kwargs[remove_key].present?
    classes = (classes.split - kwargs[remove_key].split).join(" ")
    kwargs.delete(remove_key)
  end

  # simple_form sometimes uses array of classes instead of strings
  # so we have to account for that
  if kwargs[class_key].is_a?(Array)
    kwargs[class_key] = kwargs[class_key].map(&:to_s).join(" ")
  end

  kwargs[class_key] = (classes.split + kwargs[class_key].to_s.split)
  kwargs[class_key] = kwargs[class_key].join(" ")

  kwargs
end
```

The only non-obvious line is hopefully the one with the comment – it is here because we found out that Simple Form uses arrays of strings  instead of a space-separated string in certain cases so we had to unify the two.

With these methods in the custom builder, repeating the ”bold label“ example above:

```slim
= simple_form_for(@user) do |f|
  = f.input :e_mail, label: "Important e-mail", \
                     label_html: { remove_default_class: "font-medium", \
                                   class: "font-bold" }
```

…finally produces the correct HTML with the default classes applied **except for the default `"font-medium"` class which is replaced by `"font-bold"`**. And the label finally gets rendered bold, phew!

![The proper HTML is generated now](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uc6ooq3i1o8tfjub2kdc.png)
 
## Miscellaneous goodies

Using a custom builder allows to play with the form building API in unlimited ways. For example, as we’ve said above, we lay out our admin forms in a 12-column grid. To support spanning columns in a convenient way:

```slim
= f.input :e_mail, col_span: 4
```

…we defined a private helper method in the builder that translates this custom `col_span` option to the [Tailwind `col-span-X` class](https://tailwindcss.com/docs/grid-column#spanning-columns) on the wrapper div.

We added more syntactic sugar to e.g. autofocus the first field of the form or to add some `data` attributes to connect a Stimulus controller for certain special input types. The options are endless 🙂.

## (An almost) complete form builder example

We saw a few requests to publish some more of the actual code of our custom form builder. OK, please have a look at the [`TailwindFormBuilder` at this gist](https://gist.github.com/borama/9054e85391d1aef37a7a5347a32e5574)! The builder works as a thin wrapper around the default Simple Form builder. It cooperates closely with the style-less Simple Form wrapper configuration [mentioned above](https://dev.to/nejremeslnici/styling-simple-form-forms-with-tailwind-4pel#the-styleless-wrapper-configuration). 

The builder defines a few methods that override methods in the Simple Form default builder. Their main purpose is that they add a set of predefined Tailwind classes to the various elements of the form field wrapper and to the field itself. Still, the methods allow to override every default class involved, by the `remove_default_class` and `class` parameters [discussed above](https://dev.to/nejremeslnici/styling-simple-form-forms-with-tailwind-4pel#the-custom-form-builder). Just note that in the gist there is a slightly expanded version of the helper method `arguments_with_updated_default_class` than in the sample listing above.

Please also note that the default classes defined in the builder **are by no means complete** and you will have to fill them in to make the builder actually usable. The reason is that we use Tailwind UI for styling the form components and [its license](https://tailwindui.com/license) (as far as we understand) does not allow us to publish  the whole styling as the builder is a ”derivative library“ rather than an end-product.

To use this custom builder, you can just name it together with the custom wrapper when rendering a Simple Form form:

```slim
/ app/views/users/edit.html.slim
= simple_form_for(@user, builder: Builders::TailwindFormBuilder, wrapper: :plain, ...) do |f|
  = f.input(:name)
```

Or, to use it even more conveniently, you can define a Rails helper to set the custom form builder and wrapper for us as well as style the form tag itself, for example in a 12-columns grid:

```ruby
# app/helpers/form_helper.rb
module FormHelper

  def tailwind_form_for(object, **options, &block)
    default_form_class = "grid grid-cols-1 gap-x-4 gap-y-6 items-start sm:grid-cols-12"
    options[:html] ||= {}
    options[:html][:class] = default_form_class # you can even use arguments_with_updated_default_class here to make the default classes more flexible
    options[:wrapper] = :plain
    options[:builder] = Builders::TailwindFormBuilder

    simple_form_for(object, **options, &block)
  end

end
```

Then you can just call this helper instead in a form template:

```slim
/ app/views/users/edit.html.slim
= tailwind_form_for(@user, ...) do |f|
  = f.input(:name)
```

Please get in touch with us if something isn’t clear enough, we can pair a bit or something…

## Summary

Using a style-less configuration and a custom form builder, we were able to **create a system for styling our Simple Form forms with Tailwind**, without giving up any flexibility and without resorting to monkey-patches. This combo lives happily on our production web now. If you try a similar approach, please drop us a note!

_If you don’t want to miss future posts like this, follow me here or [on Twitter](https://twitter.com/boramacz). Cheers!_