Back

Dynamic Colors using CSS Variables and a Tiny Bit of Javascript (drupalSettings)

tjheffner

2018-12-13


The Short of It:

First, what do I mean by dynamic colors? I’m referring to a color defined by a content editor, a color that the design system doesn’t necessarily know about beforehand.

CSS Variables are great, and supported in most modern browsers, except for IE11. Calling IE11 a modern browser feels dirty, but that’s neither here nor there.

For this demo, we’ll be using Drupal’s Color field to define the colors of elements on a given page. In practical terms, we have a taxonomy vocabulary called “Category”. Each category term can define a color.

When a given content type references a category term, that category’s color field output will be preprocessed & added to drupalSettings. Once available as a drupalSetting, it can easily be assigned to a CSS variable, using a couple lines of javascript.

That CSS Variable, once defined, will trickle down to the components that need it, thanks to the power of… cascading style sheets.

The Long of It:

Drupal Fields First things first, we’re gonna make sure we have a color to reference, and a node that uses it. So we have a taxonomy term with a color field (machine name: field_color). We also have a content type with that takes an optional reference to that taxonomy term’s vocabulary (machine name: field_category).

Drupal Preprocessing Now in the preprocess, we need to extract the color field value from that entity reference, and attach it to the drupalSettings object. We can do this like so, in my_module.module:

/**
 * Implements hook_page_attachments_alter().
 */
function my_module_page_attachments_alter(&$page) {
  // Makes the section color value available to javascript for live-updating.
  $node = \Drupal::routeMatch()->getParameter('node');
  if ($node && $node instanceof NodeInterface) {
    $category = $node->get('field_category')->entity;

    if (isset($category)) {
      $color = $category->field_color->color;

      $page['#attached']['drupalSettings']['my_module']['page_color'] = $color;
      $page['#attached']['library'][] = 'my_module/sections';
    }
  }
}

If we’re on a node, we check for the category field, and if that reference exists, we grab the hex string value from the color field. That gets assigned to the drupalSettings object. We also attach a js library that makes the dynamic value available to our stylesheet.

That library looks like this, inside of my_module.libraries.yml:

sections:
  version: 1.x
  js:
    js/sections.js: {}
  dependencies:
    - core/jquery

We’ll come back to the JS in a bit, once we’ve actually set up how our CSS variables should be used.

CSS Setup

CSS Variables (aka CSS Custom Properties) are a cool “new” feature that bring the concept of variables to CSS natively. They cascade and inherit as other CSS rules do, and so CSS Variables should be defined in a selector that sets their scope. We’re interested in a global scope, because these colors need to be available to many components, so I set them on the :root selector. The body element would also be global scope, but I have other body-specific styles there, and I like the separation of concerns.

// css variable set at :root for overriding section colors
// based on the value of the Category taxonomy.
:root {
  --section-color: $c-blue;
}

We set a default color for the variable so that if a category isn’t referenced on a node (or if a user has javascript disabled), there’s still a default color.

I’ll only be covering how one component (a hero) can use this CSS var, but it’s the same concept for all components that need a dynamic color.

relevant portion of code:

/**
* Hero with a solid blue background instead of an image.
*/
&--solid {
    background: $c-blue;
    background: var(--section-color);

    ...

    .hero__cta {
      background-color: $c-blue;
      background-color: var(--section-color);
      border: $border-thin solid $c-white;

      &:hover {
        color: $c-blue;
        color: var(--section-color);
        background-color: $c-white;
      }
    }
}

You’ll notice that I’ve duplicated some rules for colors. This is purely to satisfy IE11 and is not necessary if you don’t need to support it. This project did, so it was necessary. Because browsers drop CSS rules they don’t support, IE ignores the second declaration and falls back to the first rule without problems. Browsers that do support CSS variables (e.g. the rest of them), read both rules and use the last-defined, which happens to be our desired CSS variable. Everyone’s happy.

JS Connection

The final piece we need to make this all work as intended is updating the initial CSS variable declaration to be our desired color.

:root {
  --section-color: $c-blue;
}

We can do this in a few short lines of javascript, like so:

(function pageColors($, Drupal) {
  Drupal.behaviors.setPageColor = {
    attach: function getPageColor(context, settings) {
      if (settings.my_module) {
        if (settings.my_module.page_color) {
          // Grabs the page color value set in my_module_page_attachments_alter().
          const pageColor = settings.msk_content.page_color;

          // Set root CSS variable to page color. Page elements that need to be color-coded
          // use this variable and helper classes to ensure correct color applications.
          document.documentElement.style.setProperty('--section-color', pageColor);
        }
      }
    }
  }
}(jQuery, Drupal));

Tada! Now we have some dynamic connectivity between our Drupal Color field and our stylesheets. If you don’t need to support IE11, you’re done at this point. If you do need to support IE11… you’ll need a few more lines of javascript, as all the elements that should be receiving dynamic colors don’t have any clue about the CSS Variable! So we’ll have to hard code them as part of this function. Using our previous hero example, the complete function looks something like this:

(function pageColors($, Drupal) {
  Drupal.behaviors.setPageColor = {
    attach: function getPageColor(context, settings) {
      if (settings.my_module) {
        if (settings.my_module.page_color) {
          // Grabs the page color value set in my_module_page_attachments_alter().
          const pageColor = settings.msk_content.page_color;

          // Set root CSS variable to page color. Page elements that need to be color-coded
          // use this variable and helper classes to ensure correct color applications.
          document.documentElement.style.setProperty('--section-color', pageColor);

          // IE11 can't read css variables at all: https://caniuse.com/#search=css%20variables
          // So we hard color any items that need it.
          const isIE = '-ms-scroll-limit' in document.documentElement.style && '-ms-ime-align' in document.documentElement.style;
          if (isIE) {

            $('.hero--solid').css('background', pageColor);
            $('.hero--solid .hero__cta')
                .css('border', '3px solid ' + pageColor)
                .css('color', pageColor)
                .hover(function() {
                  // mouseenter
                  $(this).css('color', '#ffffff');
                  $(this).css('background-color', pageColor);
                }, function() {
                  // mouseleave
                  $(this).css('color', pageColor);
                  $(this).css('background-color', '#ffffff');
                }
             );
          }
       }
    }
  }
}(jQuery, Drupal));

Not the cleanest, but that’s IE for you. This could be cleaned up further to use string interpolation and fat arrows, if you have babel working on this file. If not, simple string concatenation is still the way to go, because IE will error on it otherwise. Maybe someday we can leave IE behind, but this post is not that day.

Anyways, that’s all there is to it. Thanks for reading!


Loading comments...
Back to top