loading...
Cover image for CSS Variables and Redis: How color customization is implemented on Forem
The DEV Team

CSS Variables and Redis: How color customization is implemented on Forem

ben profile image Ben Halpern ・3 min read

We just shipped this pull request into Forem which allows forem admins to define the core brand color of a new Forem, and I thought it was worth sharing and talking about the approach.

Allow admins to set brand color #10097

What type of PR is this? (check all applicable)

  • [ ] Refactor
  • [x] Feature
  • [ ] Bug Fix
  • [ ] Optimization
  • [ ] Documentation Update

Description

This feature allows admins to set their own main brand color....

Screen Shot 2020-08-30 at 5 00 14 PM

Screen Shot 2020-08-30 at 4 59 54 PM

Screen Shot 2020-08-30 at 4 58 44 PM

This color needs to contrast properly with white, so I added the wcag contrast gem. I think this is a good gem to add, because we can start using this elsewhere, like user profile colors, tags, etc. Currently we let users set non-contrasting colors and basically just moderate it. This will help us ensure a minimum of 4.5:1 contrast for readability and accessibility for all.

The gem has not recently been updates, but it is a fairly simple low-bloat utility gem. The gem author is active and I feel like we can be comfortable that we could merge a change if we needed to.

Also added some basic validations in the controller. I figured we can start adding some validations in this area and extract these to a better place in a future refactor once we establish more of these.

If you're not aware, Forem is the open source platform that powers DEV, a few other communities, and we will be launching much more functionality to support a broad array of communities in the coming weeks and months, getting closer to the infrastructure generalization which will make this possible en masse. Along the way we continue to make improvements to the tools available to help each forem succeed on their own terms, and exist within a diverse, but cohesive ecosystem.

Back to colors...

In building out our customization of the software, we are trying to be gradual in our approach. It's much easier to add a customization feature than to take it away later, and we don't want to get to the point where Forems are over-stuffed with features and have degrading performance.

So with that in mind, the most recent functionality that shipped is one which allows Forem admins to designate their primary brand color— which is used across the platform on buttons, and call-to-actions.

Server side implementation

We use a cached settings Ruby gem to implement the whole suite of site-wide options that admins can vary....

GitHub logo adambutler / rails-settings-cached

This is improved from rails-settings, added caching for all settings

Settings Gem

This is improved from rails-settings added caching for all settings. Settings is a plugin that makes managing a table of global key, value pairs easy. Think of it like a global Hash stored in your database that uses simple ActiveRecord like methods for manipulation. Keep track of any global setting that you dont want to hard code into your rails app. You can store any kind of object. Strings, numbers, arrays, or any object.

Status

Gem Version CI Status Code Climate codecov.io

Setup

Edit your Gemfile:

gem 'rails-settings-cached', "~> 0.5.5"

Older Rails versions:

# 4.1.x
gem "rails-settings-cached", "~> 0.4.0"
# 4.0.x
gem "rails-settings-cached", "0.3.1"
# 3.x
gem "rails-settings-cached", "0.2.4"

Generate your settings:

$ rails g settings:install

If you want custom model name:

$ rails g settings:install MySetting

Now just put that migration in the database with:

rake db:migrate

Usage

The syntax is easy. First, lets create some settings to keep…

So in adding this feature, all we had to do is add a line to the config.

field :primary_brand_color_hex, type: :string, default: "#3b49df"

But there are more concerns than this on the server side, we need to validate that value matched the proper hex pattern of /\A#(\h{6}|\h{3})\z/. This was pretty straightforward and can be seen in the pull request. The more interesting part was validating for an accessible color contrast.

In the current functionality, the color needs to be dark enough to contrast against white (e.g. the button text color), so we imported a gem for that...

GitHub logo mkdynamic / wcag_color_contrast

Calculate the WCAG contrast ratio between 2 colors.

WCAGColorContrast

A Ruby port of the Javascript https://github.com/doochik/wcag-color-contrast.

Calculates the contrast ratio between 2 colors, for checking against the WCAG recommended contrast ratio for legibility.

Installation

Add this line to your application's Gemfile:

gem 'wcag_color_contrast'

And then execute:

$ bundle

Or install it yourself as:

$ gem install wcag_color_contrast

Usage

With 2 hex colors as strings (3 or 6 characters, case insensitive, no leading pound/hash sign):

require 'wcag_color_contrast'
WCAGColorContrast.ratio('999', 'ffffff')
#=> 2.849027755287037

Can also calculate the relative luminance of a color

WCAGColorContrast.relative_luminance('008800')
#=> 0.17608318886144392

Contributing

  1. Fork it
  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

Bitdeli Badge

In order to simplify our interface and ensure that we don't need to change too much in the code should this dependency become unreliable in the future, we also established a Color::Accessibility object which can be used wherever we need it.

module Color
  class Accessibility
    def initialize(hex)
      @hex = hex.delete("#")
    end

    def low_contrast?(compared_color = "ffffff", min_contrast = 4.5)
      WCAGColorContrast.ratio(@hex, compared_color.delete("#")) < min_contrast
    end
  end
end

Further uses of this could be validations on user-defined profile colors. Currently we have some functionality to try and ensure contrast, but it is a poor implementation and we cannot guarantee the WCAG standard of 4.5:1 contrast ratio on profile colors.

Front end implementation

The CSS required to make this work is quite minimal... because of the great work that had gone into our CSS system prior to this functionality. We have already established a set of CSS variables used throughout the site, so that we only have to change a few lines in the application and everything else should fall in line.

This snippet was added directly into the html.erb file which is inserted into the head of the document...

<style>
  :root {
    --accent-brand: <%= SiteConfig.primary_brand_color_hex %>;
    --accent-brand-darker: <%= HexComparer.new([SiteConfig.primary_brand_color_hex]).brightness(0.85) %>;
    --accent-brand-lighter: <%= HexComparer.new([SiteConfig.primary_brand_color_hex]).brightness(1.1) %>;
    --accent-brand-a10: #{rgba(<%= SiteConfig.primary_brand_color_hex %>, 0.1)};
  }
</style>

And that is all it takes for the rest of the CSS to respect the --accent-brand variables throughout. Check out the above pull request to take a peak at all the code and conversation that went into this change.

Happy coding!

Posted on by:

ben profile

Ben Halpern

@ben

A Canadian software developer who thinks he’s funny. He/Him.

The DEV Team

The team behind this very platform. 😄

Discussion

markdown guide
 

This is very cool! Did you consider setting the default font colour for buttons etc to black when the chosen brand colour didn't meet the necessary contrast with white text? Just wondering as I've noticed in working with the React material-ui library this is what they do to adjust to the primary brand colour 🤔

 

Currently it only allows “dark” colors, but yeah I think a follow up will be to allow light colors and likewise change the color to black (or sufficiently dark complementary colors.)

 

Nice, and the way you guys have implemented it is totally open to that kind of extension later.

I love stuff like this that makes users' lives easier and promotes accessibility at the same time ♥️

 

Very cool Ben. I feel good that contrast thing sort of matches the one I threw together a couple of years ago 😅

Comment for #735

I've not ever written ruby before so feel free to write something yourself but something like this is what I was thinking

def contrast_verified_hex(fg, bg)
  # WCAG20: https://www.w3.org/TR/WCAG20/#visual-audio-contrast
  f = fg[1..-1].chars.each_slice(2).map(&:join).map {|h| h.hex / 255.0}
  b = bg[1..-1].chars.each_slice(2).map(&:join).map {|h| h.hex / 255.0}
  
  # Formula: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
  def RL(d) d <= 0.03928 ? d / 12.92 : ((d + 0.055) / 1.055) ** 2.4 end
  
  # Formula: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
  fg_lum = 0.2126 * RL(f[0]) + 0.7152 * RL(f[1]) + 0.0722 * RL(f[2]) + 0.05
  bg_lum = 0.2126 * RL(b[0]) + 0.7152 * RL(b[1]) + 0.0722 * RL(b[2]) + 0.05
  
  # Formula: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
  ratio = fg_lum < bg_lum ? bg_lum / fg_lum : fg_lum / bg_lum

  # The visual presentation of text and images of text has a contrast ratio of at least 7:1 (WCAG20)
  case  
  when ratio >= 7
    return fg
  when bg_lum < 0.5
    return "#FFFFFF"
  else  
    return "#000000"
  end
end

Then in user_decorator.rb you could say something like

def enriched_colors
    if bg_color_hex.blank?
      {
        bg: assigned_color[:bg],
        text: assigned_color[:text]
      }
    else
      {
        bg: bg_color_hex || assigned_color[:bg],
        text: contrast_verified_hex(text_color_hex, bg_color_hex) || assigned_color[:text]
      }
    end
  end