Benito Serna Tips and tools for Ruby on Rails developers

Styler, a tool to compose css classes with ruby

December 7, 2021

One of the things that I want from css is to have to possibility to compose already defined styles, to define new ones…

If you try to write “Semantic CSS”, you will find a hard time trying to avoid the repetition on things that look the same but are different things, like when you want to render a “card” for the an author and then for an article.

You can create “content agnostic CSS components”, but things start to get complicated, when you want to avoid duplication if things from one component are similar to other components.

One way of solving this problems is by using something like the @extend function from sass, or the @apply function from Tailwind css, but both tools are recommended just for some specific cases and not to build your styles on top of them (@extend, @apply).

A tool to compose css classes

This is why I have been working on this Styler, a tool to compose css classes from other classes an groups of classes.

Is a tool, to extend what the utility css classes can give us. A tool to build on top of frameworks like Tachyons or Tailwind css.

Define styles

You can declare styles with a name and an already defined css class, for example if you define a style named btn, like this…

styles = Styler.new do
  style :btn, ["white", "bg_blue"]
end

That will return an object where you can call btn on it…

styles.btn.to_s # => "white bg_blue"

You would be able to use it like this in your erb files…

<button class="<%= styles.btn %>">My button</button>

or in haml…

%button{class: styles.btn} My button

To output…

<button class="white bg_blue">My button</button>

Compose styles

You can define many of this styles and compose them…

styles = Styler.new do
  style :btn, ["padding3", "marin3"]
  style :blue_btn, [btn, "white", "bg_blue"]
end
%button{class: styles.btn} My button
%button{class: styles.blue_btn} My button

And the output would be…

<button class="padding3 margin3">My button</button>
<button class="padding3 margin3 white bg_blue">My button</button>

Substract styles

By composing your styles you can also substract classes from previous styles, like this…

styles = Styler.new do
  style :default, ["pa3", "white", "bg_blue"]
  style :danger, [default - "bg_blue", "bg_red"]
end

styles.default.to_s # => "pa3 white bg_blue"
styles.danger.to_s # => "pa3 white bg_red"

Passing arguments to styles

You can also define styles that expect an argument, to help you decide which styles to display, like this…

styles = Styler.new do
  style :default_color do |project|
    if project[:color] == "blue"
      ["bg_blue"]
    else
      ["bg_red"]
    end
  end
end

project = { color: "blue" }
styles.default_color(project).to_s # => "bg_blue"

And you can use this styles to build other styles, like this…

project = { color: "blue" }

styles = Styler.new do
  style :default_color do |project|
    if project[:color] == "blue"
      ["bg_blue"]
    else
      ["bg_red"]
    end
  end

  style :title, [default_color(project), "pa3"]
end

styles.title(project).to_s # => "bg_blue pa3"

Or like this…

styles = Styler.new do
  style :default_color do |project|
    if project[:color] == "blue"
      ["bg_blue"]
    else
      ["bg_red"]
    end
  end

  style :title do |project|
    [default_color(project), "pa3"]
  end
end

project = { color: "blue" }
styles.title(project).to_s # => "bg_blue pa3"

Define collections

You can define collections as namespaces for your styles…

styles = Styler.new do
  collection :buttons do
    style :default, ["pa3", "blue"]
    style :danger, [default - "blue", "red"]
  end
end

styles.respond_to?(:default) # => false
styles.respond_to?(:danger) # => false
styles.buttons.default.to_s # => "pa3 blue"
styles.buttons.danger.to_s # => "pa3 red"

Nested collections

You can define nested collections to build complete themes…

styles = Styler.new do
  collection :v1 do
    collection :buttons do
      style :default, ["pa3", "blue"]
    end
  end

  collection :v2 do
    collection :buttons do
      style :default, ["pa3", "red"]
    end
  end
end

styles.v1.buttons.default.to_s # => "pa3 blue"
styles.v2.buttons.default.to_s # => "pa3 red"

Define collection with arguments

Like with the styles, you can define collections that require arguments, like this..

styles = Styler.new do
  collection :buttons do |project|
    if project[:color] == "blue"
      style :default, ["pa3", "blue"]
    else
      style :default, ["pa3", "red"]
    end
  end
end

project = { color: "blue" }
styles.buttons(project).default.to_s # => "pa3 blue"

Select a collection with an alias

And you can pick one of those collections, by using a collection_alias

styles = Styler.new do
  collection :v1 do
    collection :buttons do
      style :default, ["pa3", "blue"]
    end
  end

  collection :v2 do
    collection :buttons do
      style :default, ["pa3", "red"]
    end
  end

  collection_alias :theme, v1
end

styles.theme.buttons.default.to_s # => "pa3 blue"

Collection alias with a block

If you need you can select the collection alias dynamically with a block…

styles = Styler.new do
  collection :v1 do
    collection :buttons do
      style :default, ["pa3", "blue"]
    end
  end

  collection :v2 do
    collection :buttons do
      style :default, ["pa3", "red"]
    end
  end

  collection_alias :theme do |current_version|
    if current_version == "v1"
      v1
    else
      v2
    end
  end
end

styles.theme("v1").buttons.default.to_s # => "pa3 blue"
styles.theme("v2").buttons.default.to_s # => "pa3 red"

Select a collection from other styler

You can also define your collections on different “stylers” and be able to declare an alias to access them…

v1 = Styler.new do
  collection :buttons do
    style :default, ["pa3", "blue"]
  end
end

v2 = Styler.new do
  collection :buttons do
    style :default, ["pa3", "red"]
  end
end

styles = Styler.new do
  collection_alias :theme do |current_version|
    if current_version == "v1"
      v1
    else
      v2
    end
  end
end

styles.theme("v1").buttons.default.to_s # => "pa3 blue"
styles.theme("v2").buttons.default.to_s # => "pa3 red"

Copy styles from collection

If you need to use the styles from other collection, but you need to override some of them, you can copy the styles and then override what you want.

styles = Styler.new do
  collection :v1 do
    collection :buttons do
      style :default, ["pa3", "blue"]
      style :danger, [default - "blue", "red"]
    end
  end

  collection :v2 do
    collection :buttons do
      copy_styles_from collection: v1.buttons
      style :danger, [v1.buttons.danger - "red", "orange"]
    end
  end
end

expect(styles.v2.buttons.default.to_s).to eq "pa3 blue"
expect(styles.v2.buttons.danger.to_s).to eq "pa3 orange"

Read the source, Luke

If you want to learn more you can read the source on github, and maybe take a look a the specs to find more examples.

Library status… It would be nice to receive some feedback.

I have been working on this tool for some weeks, and I have been using it on some personal projects and to build a collection with the tachyons components.

I have pitched the library to my team at briq.mx (we use Tachyons), but we are not using it yet…

Is still a concept, and would be nice to receive some feedback from you!

Installation

If you want to play with this tool in a project you can install it by adding this line to your application’s Gemfile:

gem "ruby_styler"

And then execute:

$ bundle install

Or install it yourself as:

$ gem install styler

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.