Class variants

We ❤️ Tailwind CSS but sometimes it’s difficult to manage the state of some elements using conditionals. class_variants is a tiny helper that should enable you to create, configure, and apply different variants of elements as classes.

Inspired by variant-classnames ✌️

Quicklinks

Installation

Add this line to your application’s Gemfile:

gem 'class_variants'

And then execute:

$ bundle

Or install it yourself as:

$ gem install class_variants

Usage

We create an object from the class or helper where we define the configuration using four arguments:

  1. The base keyword argument with default classes that should be applied to each variant.
  2. The variants keyword argument where we declare the variants with their option and classes.
  3. The compound_variants keyword argument where we declare the compound variants with their conditions and classes
  4. The defaults keyword argument (optional) where we declare the default value for each variant.

Below we’ll implement the button component from Tailwind UI.

# Define the variants and defaults
button_classes = ClassVariants.build(
  base: "inline-flex items-center rounded border border-transparent font-medium text-white hover:text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2",
  variants: {
    color: {
      indigo: "bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500",
      red: "bg-red-600 hover:bg-red-700 focus:ring-red-500",
      blue: "bg-blue-600 hover:bg-blue-700 focus:ring-blue-500",
    },
    size: {
      sm: "px-2.5 py-1.5 text-xs",
      md: "px-3 py-2 text-sm",
      lg: "px-4 py-2 text-sm",
      xl: "px-4 py-2 text-base",
    },
    compound_variants: [
      { color: :red,  border: true, class: "border-red-800"  },
      { color: :blue, border: true, class: "border-blue-800" }
    ]
    # A variant whose value is a string will be expanded into a hash that looks
    # like  { true => "classes" }
    icon: "w-full justify-center",
    # Unless the key starts with !, in which case it will expand into
    # { false => "classes" }
    "!icon": "w-auto",
  },
  defaults: {
    size: :md,
    color: :indigo,
    icon: false
  }
)

# Call it with our desired variants
button_classes.render(color: :blue, size: :sm)
button_classes.render
button_classes.render(color: :red, size: :xl, icon: true)

Compound Variants

button_classes = ClassVariants.build(
  base: "inline-flex items-center rounded",
  variants: {
    color: {
      red:  "bg-red-600",
      blue: "bg-blue-600",
    },
    border: "border"
  },
  compound_variants: [
    { color: :red,  border: true, class: "border-red-800"  },
    { color: :blue, border: true, class: "border-blue-800" }
  ]
)

button_classes.render(color: :red) # => "inline-flex items-center rounded bg-red-600"
button_classes.render(color: :red, border: true) # => "inline-flex items-center rounded bg-red-600 border border-red-600"

Override classes with render

We can also override the builder classes in the render method.

button_classes = ClassVariants.build(
  base: "inline-flex items-center rounded",
  variants: { ... },
)

button_classes.render(color: :red, class: "block")

Now, the block class will be appended to the classes bus.

If you’re using the tailwind_merge plugin it will override the inline-flex class.

Block configurations

You might have scenarios where you have more advanced conditionals and you’d like to configure the classes using the block notation.

alert_classes = ClassVariants.build do
  # base
  base "..."

  # variant
  variant color: :red, class: "..."

  # compound variant
  variant type: :button, color: :red, class: "..."

  # defaults
  defaults color: :red, type: :button
end

# usage
alert_classes.render(color: :red, type: :button)

Slots

You might have components which have multiple slots or places where you’d like to use conditional classes.
class_variants supports that through slots.

# Example

alert_classes = ClassVariants.build do
  # base with slots
  base do
    slot :head, class: "..."
    slot :body, class: "..."
  end

  # variant with slots
  variant color: :red do
    slot :head, class: "..."
    slot :body, class: "..."
  end

  # compound variant with slots
  variant type: :button, color: :red do
    slot :head, class: "..."
    slot :body, class: "..."
  end

  # set defaults
  defaults color: :red, type: :button
end
<div>
  <div class="<%= alert_classes.render(:head) %>">
    Head of alert
  </div>
  <div class="<%= alert_classes.render(:body) %>">
    Body of alert
  </div>
</div>

Full API

# Configuration
alert_classes = ClassVariants.build(
  base: "...",
  variants: {
    color: {
      red: "...",
      black: "..."
    },
    type: {
      button: "...",
      link: "..."
    }
  },
  compound_variants: [],
  defaults: {
    color: :red,
    type: :button
  }
) do
  # base without slots
  base "..."

  # base with slots
  base do
    slot :head, class: "..."
    slot :body, class: "..."
  end

  # variant without slots
  variant color: :red, class: "..."

  # variant with slots
  variant color: :red do
    slot :head, class: "..."
    slot :body, class: "..."
  end

  # compound variant without slots
  variant type: :button, color: :red, class: "..."

  # compound variant with slots
  variant type: :button, color: :red do
    slot :head, class: "..."
    slot :body, class: "..."
  end

  # option 1 (my favorite)
  defaults color: :red, type: :button

  # option 2
  defaults do
    color :red
    type :button
  end
end

# Usage

# renders the defaults
alert_classes.render

# render default slot with custom variants
alert_classes.render(color: :red)

# render slot with defaults variants
alert_classes.render(:body)

# render slot with custom variants
alert_classes.render(:body, color: :red)

# if slot not exist, throw error? return empty classes?
alert_classes.render(:non_existent_slot, color: :red)

# render default slot with custom class (will be merged)
alert_classes.render(class: "...")

# render slot with custom class (will be merged)
alert_classes.render(:body, class: "...")

Use with Rails

# Somewhere in your helpers
def button_classes(classes, **args)
  class_variants(
    base: "inline-flex items-center rounded border border-transparent font-medium text-white hover:text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2",
    variants: {
      size: {
        sm: "px-2.5 py-1.5 text-xs",
        md: "px-3 py-2 text-sm",
        lg: "px-4 py-2 text-sm",
        xl: "px-4 py-2 text-base",
      },
      color: {
        indigo: "bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500",
        red: "bg-red-600 hover:bg-red-700 focus:ring-red-500",
        blue: "bg-blue-600 hover:bg-blue-700 focus:ring-blue-500",
      },
    },
    defaults: {
      size: :md,
      color: :indigo,
    }
  )
end
&lt;%= link_to :Avo, "https://avohq.io", class: button_classes.render(color: :blue, size: :sm) %&gt;
&lt;%= link_to :Avo, "https://avohq.io", class: button_classes.render %&gt;
&lt;%= link_to :Avo, "https://avohq.io", class: button_classes.render(color: :red, size: :xl) %&gt;

Helper module

If you’re developing something more complex you might want to use composition more. You might want to use the helper module for that.

class MyClass
  include ClassVariants::Helper

  class_variants {
    base: {},
    variants: {}
  }
end

MyClass.new.class_variants(:container, color: :red, class: "shadow")

tailwind_merge

By default, the classes are merged using concat, but you can use the awesome TailwindMerge gem.
Install the gem using bundle add tailwind_merge and use this configuration to enable it.

ClassVariants.configure do |config|
  config.process_classes_with do |classes|
    TailwindMerge::Merger.new.merge(classes)
  end
end

Output

Other packages

Try Avo ⭐️

If you enjoyed this gem try out Avo. It helps developers build Internal Tools, Admin Panels, CMSes, CRMs, and any other type of Business Apps 10x faster on top of Ruby on Rails.

Contributing

  1. Fork it git clone https://github.com/avo-hq/class_variants
  2. Create your feature branch git checkout -b my-new-feature
  3. Commit your changes git commit -am 'Add some feature'
  4. Push to the branch git push origin my-new-feature
  5. Create new Pull Request

License

This package is available as open source under the terms of the MIT License.

Cutting a release

# Build
gem build class_variants.gemspec -o latest.gem
# Publish
gem push --host https://rubygems.org/ ./latest.gem
# Cut a tag
git tag v0.0.6 -a -m "Version 0.0.6"
# Push tag to repo
git push --follow-tags
# Go to the repo and generate release from tag