Search code examples
cssthemescss-variables

Are there best practices for creating themeable component variants based on CSS custom properties?


I'm currently working with a team developing a bunch of web components, supposed to be the basis for a current and also upcoming web applications developed in the customer's company.

I want these components to support color themeing; the first use-case for this being supporting a dark mode.

Current approach

  • I have defined the customer's corporate design colors in a file colorpalette.css (the colors are just an example):

    :root {
        --color-blue: #004994;
        --color-light-blue: #0095db;
        --color-light-green: #afca06;
        --color-light-grey: #9c9c9c;
        --color-green: #51ae31;
        --color-grey: #555555;
        --color-orange: #f39200;
        --color-red: #d40f14;
        --color-turquoise: #008c8e;
        --color-violet: #b80d78;
        --color-yellow: #ffcc00;
        --color-white: #ffffff;
        --color-black: #000000;
    }
    
  • I'm making sure all other files using or defining colors only use colors from this file. This leads to a file colors.css where I start defining different colors per theme:

    @import "./colorpalette.css";
    :root {
        --bg-color-button: var(--color-light-grey);
        --text-color-button: var(--color-black);
        --border-color-button: var(--color-grey);
    }
    
    :root[data-theme=dark] {
        --bg-color-button: var(--color-grey);
        --text-color-button: var(--color-white);
        --border-color-button: var(--color-light-grey);
    }
    

So far, I'm satisfied with this approach.

Problem

Now I'm starting to add different semantic versions of these buttons: .btn-danger, .btn-success, etc. which are supposed to use color as a means of transporting these semantics.

Possible solutions - two different alternatives

And here is where I'm uncertain as of which way to pursue to achieve this, as I see 2 alternatives:

  1. Create new variables for these semantics, carrying the semantic in their name:

    :root {
        --bg-color-button-success: var(--color-light-green);
        --text-color-button-success: var(--color-black);
        --border-color-button-success: var(--color-green);
    }
    

    and use these in the button style definitions:

    .btn {
        background-color: var(--bg-color-button);
        color: var(--text-color-button);
        border-color: var(--border-color-button);
    }
    .btn.btn-success {
        background-color: var(--bg-color-button-success);
        color: var(--text-color-button-success);
        border-color: var(--border-color-button-sucess);
    }
    
    • Pros
      • You can re-theme the whole application by modifying only two files.
    • Cons
      • This approach obviously results in a myriad of variables, as we need to define a separate variable for each place where we need to apply a different color.
      • If you later want to introduce additional button variations, you'd have to edit both colors.css and button.css.
  2. The other approach could be redefining the existing variables in the specified context:

    .btn {
        background-color: var(--bg-color-button);
        color: var(--text-color-button);
        border-color: var(--border-color-button);
    }
    .btn.btn-success {
        --bg-color-button: var(--color-light-green);
        --text-color-button: var(--color-black);
        --border-color-button: var(--color-green);
    }
    
    • Pros
      • alot less variables.
      • Variables only get redefined in places where they're actually used.
      • Defining new button variants is easier than in the other approach as you only need to edit button.css.
    • Cons
      • To retheme the component library, you will probably have to touch every single component's css file in addition to colorpalette.css and colors.css.

I've tried to sum up the pros and cons I see for both approaches.

Question

Which would be other reasons to prefer one over the other way?

Hint

Please consider the following before eventually voting to close:

I know many might see this question as off-topic because it attracts opinionated answers, but I'm sure this question has alot of practical relevance for many developers as css custom properties have only recently become the de-facto standard, which usually results in best practices emerging only slowly. So opinionated answers along with reasons for these opinions are exactly what I'm asking here intentionally.

This is also in no way a duplicate of Best Practices - CSS Theming because that question is from 2010 where css custom properties didn't exist.


Solution

  • In my opinion, the priority requirement of a theme system is easily changing the appearance. So your first solution would be suitable for this case. Let's talk about its cons.

    This approach obviously results in a myriad of variables, as we need to define a separate variable for each place where we need to apply a different color.

    More variables are needed. Cause they serve different purposes. And when you re-design your theme, it will help you quickly change the appearance without traveling into all the components files. And you only need to care about it just in case re-design the theme. So I think it should not be counted as cons.

    If you later want to introduce additional button variations, you'd have to edit both colors.css and button.css.

    Again, I don't consider this is cons because you just need to add some extra code in 2 files. And these codes will not affect anything that exists before.

    But you forgot to mention the biggest con of this approach. It is you need to rewrite the CSS rules. What's happens if we use a gradient background? The code will be something like that:

    .btn {
        background: linear-gradient(to left, var(--text-color-button-1), var(--text-color-button-2) 50%, var(--text-color-button-3) 75%, var(--text-color-button-4) 75%);
        color: var(--text-color-button);
        border-color: var(--border-color-button);
    }
    .btn.btn-success {
        background: linear-gradient(to left, var(--text-color-button-success-1), var(--text-color-button-success-2) 50%, var(--text-color-button-success-3) 75%, var(--text-color-button-success-4) 75%);
        color: var(--text-color-button-success);
        border-color: var(--border-color-button-sucess);
    }
    

    It will be a nightmare if we want to change the number 50% to another. So let's combine your two approaches into one, to fix their cons.

    This is the solution for those who want to quick scanning:

    color.css

    --color-red-100: red;
    --color-red-500: red;
    --color-red-900: red;
    --color-green-100: green;
    --color-green-500: green;
    --color-green-900: green;
    
    --color-bg-button-1: var(--color-red-100);
    --color-bg-button-2: var(--color-red-500);
    --color-bg-button-3: var(--color-red-900);
    --color-bg-button-4: var(--color-red-100);
    
    --color-bg-button-success-1: var(--color-green-100);
    --color-bg-button-success-2: var(--color-green-500);
    --color-bg-button-success-3: var(--color-green-900);
    --color-bg-button-success-4: var(--color-green-100);
    

    button.css

    .btn {
        background: linear-gradient(to left, var(--text-color-button-1), var(--text-color-button-2) 50%, var(--text-color-button-3) 75%, var(--text-color-button-4) 75%);
    }
    .btn.btn-success {
        --text-color-button-1: var(--color-bg-button-success-1);
        --text-color-button-2: var(--color-bg-button-success-2);
        --text-color-button-3: var(--color-bg-button-success-3);
        --text-color-button-4: var(--color-bg-button-success-4);
    }
    

    In this approach:

    • By using base color variables, if you just want to adjust some shades of grey, you just need to change the base color variables, button and anything else will be changed accordingly.
    • By using scoped variable but linking to a new variable, you don't need to rewrite the CSS rules but can easily re-design the theme just by editing the value of variables in one file.
    • Of course, this approach still has the two cons of your first solution but I think it can be accepted for higher priority requirements.

    For me, the best practice should be put in a specific context, so this solution is just fit in case your first priority is easily re-design the theme. If you just want to use CSS variables to easily reuse and don't have any plan to change the value of the variables so the OP's 2nd solution should be picked for fast programming.