Bootstrap SASS variable override challenge

11,666

Solution 1

Solved, but I don’t know from which version this works. I believe the solution could have always been available. Tested on:

> sassc --version

sassc: 3.2.1
libsass: 3.2.5
sass2scss: 1.0.3

We are going to use a simplified environment, so filenames do not match with Bootstrap’s.


Challenge

Given a framework we do not control (for example installed only on the Continuous Integration environment and not available in our machines) that expresses SCSS variables in the following manner:

// bootstrap/_variables.scss

$brand-primary: #f00 !default;
$brand-warning: #f50 !default;

$link-color: $brand-primary !default;

And given a file in that same framework that uses the variables:

// bootstrap/main.scss

a:link, a:visited {
  color: $link-color;
}

The challenge is:

Include the framework in your own application’s SCSS in such a way that

  1. variables’ dependencies in the framework are preserved and honors;
  2. you can depend in on the default values but still be able to change the results on the framework dependencies.

More precisely:

Include the framework in your application’s SCSS in such a way that $brand-color will always be the inverse of $brand-warning, whatever its value is in the framework.

Solution

The main file would look like this:

// application.scss

@import "variables";
@import "bootstrap/variables";
@import "bootstrap/main";

And your variables file would look like this:

// _variables.scss

%scope {
  @import "bootstrap/variables";

  $brand-primary: invert($brand-warning) !global;
}

Results:

> sassc main.scss

a {
  color: blue; }

Explanation

The %scope part is not something magic of SCSS, it’s simply a hidden class with the name scope, available exclusively for later extensions with @extend. We are using it just to create a variable scope (hence the name).

Inside the scope we @import the framework’s variables. Because at this moment there’s no value for each variable every variable is created and assigned its !default value.

But here’s the gimmick. The variables are not global, but local. We can access them but they are not going to pollute the global scope, the one that will be later used to derive variables inside the framework.

In fact, when we want to define our variables, we want them global, and indeed we use the !global keyword to signal SCSS to store them in the global scope.


Caveats

There’s one major caveat: you cannot use your own variables while you define them.

That means that in this file

%scope {
  @import "bootstrap/variables";

  $brand-primary: black !global;

  @debug $brand-primary;
}

The @debug statement will print the default value defined in bootstrap/_variables.scss, not black.

Solution

Split variables in two parts:

%scope {
  @import "bootstrap/variables";

  $brand-primary: black !global;

  @debug $brand-primary;
}

@debug $brand-primary;

The second @debug will indeed correctly print black.

Solution 2

With Bootstrap 4 or bootstrap-sass all variables set in the _variables.scss with the !default flag.

Therefore, if you set a variable before bootstrap's _variables.scss is included, when it is included, the value from _variables.scss will be ignored.

So my sass entry file might look like this ...

@import "bootstrap-overrides";
@import "bootstrap/scss/bootstrap-flex";
@import "mixins/module";

Solution 3

In alpha 6 of Bootstrap 4, all variables in _variables.scss can be overridden in _custom.scss, in the way that mryarbles describes. However, the overrides do not cascade to other elements, because the inclusion order is:

@import "variables";
@import "mixins";
@import "custom";

When I change this to

@import "custom";
@import "variables";
@import "mixins";

it works as expected.

Share:
11,666

Related videos on Youtube

jbyrd
Author by

jbyrd

Updated on September 14, 2022

Comments

  • jbyrd
    jbyrd over 1 year

    EDIT: This question was marked as a duplicate of this one, but see the addendum near the end of this answer to see what that question doesn't ask, and what the answer doesn't answer.

    I'm working on a web app that uses Bootstrap 3. I have a basic 3-layer override architecture, where 1) Bootstrap's _variables.scss contains the core variables, 2) _app-variables.scss contains the base app variables that override Bootstrap's _variables.scss, and 3) _client-variables.scss contains client-specific customizations that override _app-variables.scss. Either #2 or #3 (or both) can be blank files. So here's the override order:

    _variables.scss // Bootstrap's core
    _app-variables.scss // App base
    _client-variables.scss // Client-specific
    

    Simple enough in theory, but a problem arises because of what I'll call "variable dependencies" -- where variables are defined as other variables. For example:

    $brand: blue;
    $text: $brand;
    

    Now, let's say the above variables are defined in _variables.scss. Then let's say in _app-variables.scss, I override only the $brand variable to make it red: $brand: red. Since SASS interprets the code line by line sequentially, it will first set $brand to blue, then it will set $text to blue (because $brand is blue at that point), and finally it will set $brand to red. So the end result is that changing $brand afterwards does not affect any variables that were based on the old value of $brand:

    _variables.scss
    ---------------------
    $brand: blue;
    $text: $brand; // $text = blue
    .
    .
    .
    
    _app-variables.scss
    ---------------------
    $brand: red; // this does not affect $text, b/c $text was already set to blue above.
    

    But obviously that's not what I want - I want my change of $brand to affect everything that depends on it. In order to properly override variables, I'm currently just making a full copy of _variables.scss into _app-variables.scss, and then making modifications within _app-variables from that point. And similarly I'm making a full copy of _app-variables.scss into _client-variables.scss and then making modifications within _client-variables.scss at that point. Obviously this is less than ideal (understatement) from a maintenance point of view - everytime I make a modification to _variables.scss (in the case of a Bootstrap upgrade) or _app-variables.scss, I have to manual trickle the changes down the file override stack. And plus I'm having to redeclare a ton of variables that I may not even be overriding.

    I found out that LESS has what they call "lazy loading" (http://lesscss.org/features/#variables-feature-lazy-loading), where the last definition of a variable is used everywhere, even before the last definition. I believe this would solve my problem. But does anyone know a proper variable-override solution using SASS?

    ADDENDUM:
    Here's one technique I've already thought through: include the files in reverse order, using !default for all variables (this technique was also suggested in the answer to this question). So here's how this would play out:

    _app-variables.scss
    ---------------------
    $brand: red !default; // $brand is set to red here, overriding _variables.scss's blue.
    .
    .
    .
    
    
    _variables.scss
    ---------------------
    $brand: blue !default; // brand already set in _app-variables.scss, so not overridden here.
    $text: $brand !default; // $text = red (desired behavior)
    

    So that solution is almost perfect. However, now in my override files, I don't have access to variables defined in Bootstrap's _variables.scss, which I would need if I wanted to define my variable overrides (or my own additional custom variables) using other Bootstrap variables. For example, I might want to do: $custom-var: $grid-gutter-width / 2;

    • Marcelo Somers
      Marcelo Somers over 8 years
      I have this exact issue that you outlined in the addendum, around not having access to the default variables in my overrides. Have you found a solution to this?
  • Pier Paolo Ramon
    Pier Paolo Ramon over 7 years
    @jbyrd woudl you accept this as a winning proposal :)
  • bayerphi
    bayerphi over 6 years
    You can avoid the caveat of not being able to use your own variables (which is pretty significant IMO) by defining them like this: %scope { //import "boostrap/variables"; $brand-primary: black; $brand-primary: $brand-primary !global; //debug $brand-primary; } It seems that sass does directly define the variable within the global scope if you append "!global". The only issue remaining is that this solution does only work on one level and not on a structure like: bootstrap-styles > company-styles > project-styles
  • Pier Paolo Ramon
    Pier Paolo Ramon over 6 years
    Ping @jbyrd again :)
  • jbyrd
    jbyrd about 6 years
    Just seeing this - I haven't used Sass in a while, but I do remember this challenge - and this is a very clever solution! I haven't actually tried implementing it, but it seems to makes sense, so I will accept this as the answer.
  • madz
    madz almost 6 years
    Clever solution, one issue to be aware of is if you override a bootstrap variable, from which other bootstrap variables derive (eg $gray-base just for example) then if you reference any of the derived variables (eg $gray-darker) in that same scope block you will get the original bootstrap values. Things are okay if you start another scope block with another import of bootstrap. But it means you need to know how the bootstrap variables are defined in order to do this, so you almost might as well go back to just copying and pasting their default declarations
  • Pier Paolo Ramon
    Pier Paolo Ramon almost 6 years
    @madz there’s that same issue in the caveats section of the answer, in my humble opinion. Could you check if it is actualy adherent to the case you are talking about?
  • madz
    madz almost 6 years
    @Pier yes I was sharing my experience when I tried the technique - the caveat applies to not only your own use of your overridden variables, but also any usage of the variables within the bootstrap files - when these files are imported all references are resolved using whatever the default values were. A new scope block solves this problem, but you need to take care which bootstrap variables you reference in the original scope block. That said, your answer is still the best workaround I've seen for this issue if you wish to avoid copying and pasting.