Consider a “white labeled” app where a CMS admin can customize the design of their public-facing dashboard. The developer is tasked with saving a handful of user-populated values to the database. Colors, fonts, and perhaps a background image or two now need to override dozens of CSS properties throughout the system.

Django Compressor has historically been great for this type of task. The workflow would look something like this:

  1. Build a SASS file/Django template hybrid that looks like this:
$brand-color: {{ user.brand_color }};
$link-color: {{ user.link_color }};
$font-family: {{ user.font_family }};
  1. Pipe that file into Django Compressor and build a fresh full copy of the app CSS using these variables instead of the defaults.

  2. Inject a link to the new CSS file, if it exists, underneath the base app file to let the CSS cascade override everything properly.

This is sub-optimal in 2020 for several reasons. It creates unnecessary duplicative code. It adds a potential point-of-failure on the server. Using modern CSS, there are ways to make the browser do all of the heavy lifting.

Enter CSS variables

CSS variables are a perfect fit for this task if user themes don’t need to support Internet Explorer. (If IE is mandatory, css-vars-ponyfill may help you get the job done.) With CSS variables, it’s possible to override variables using the cascade, so we can avoid the preprocessor entirely. It’s also possible to make this change in a fairly surgical way, without completely refactoring your stylesheets away from SASS or LESS. Note that this is not how I would start a new project. Instead, I would begin with CSS variables as a first-class citizen. This is simply a safe way to make this change to an older codebase.

Update the base SCSS file

First, we’ll update the base SCSS file to use CSS variables, with a fallback of the original values. The original _variables.scss file looks like this:

$heading-color: #222 !default;
$link-color: #33C !default;
$font-family: Lato, sans-serif !default;

To keep those default values and have them overridden later with CSS variables, we need to inject the CSS variable syntax:

$heading-color: var(--heading-color, #222);
$link-color: var(--link-color, #33C);
$font-family: var(--font-family, "Lato, sans-serif");

If no CSS variables are defined, the original values will be used. With these changes made, it’s time to cross your fingers and hope the CSS compiles.

It might not.

If the SCSS source uses color functions throughout the project, there will be errors. For example, SASS won’t know what to do with mix($black, $heading-color, 20%) since $heading-color no longer resolves to computable color values. These will require manual refactoring. Solutions vary greatly depending on how colors are structured in the SCSS. This could be solved with a clever use of RGBA gradient overlays, going all-in with a CSS-variable approach to colors, or making more variables in _variables.scss to consolidate this work and put all of the color math in one place. Unfortunately, SASS color functions are still much more elegant than trying to do this work in CSS, so every solution has tradeoffs.

Update the Django template

Now that the SASS is ready, the base CSS file can be compiled with modern frontend-only tools, and CSS variables would only be used for the theme overrides. The Django template could be a template file that compiles to a CSS file (which would be best for page-caching reasons), or an inlined <style> tag in the dashboard HTML template:

{% block append_head %}
<style>
:root {
    {% if user.heading_color %} 
    --heading-color: {{ user.heading_color }}; 
    {% endif %}
    {% if user.link_color %} 
    --link-color: {{ user.link_color }}; 
    {% endif %}
    {% if user.font_family %} 
    --font-family: {{ user.font_family }};
    {% endif %}
}
</style>
{% endblock append_head %}

Wrap it up, or keep going

This is a small example to begin working with CSS custom properties. It’s a powerful technology with great browser support, and it’s been a game-changer here at Lincoln Loop. So if it’s not a part of your regular tool belt yet, it’s time to give it a spin.