SASS Multiple levels of variable precedence/inheritance - css

I'm using Laravel Mix and Webpack for SASS pre-processing.
I have two "themes" in my website which I want to be lean, inheriting variables where they need to. For example, my primary theme will include in this order:
// Primary theme
#import "./primary-variables.scss";
#import "/path/to/default/theme/main.scss";
My default theme would look like this:
// Default theme
#import "./default-variables.scss";
#import "~bootstrap-sass/assets/stylesheets/_bootstrap";
Similarly to this question, I've included the primary variables first, then the default theme variables, then bootstrap last.
In my default theme I add !default to all variables so where they are redefining Bootstrap they will be used in priority, and where new they will be a default value. The primary theme doesn't use !default at all.
Working example
If Bootstrap defines $brand-danger as say red !default, my default theme redefines it as blue !default and my primary theme redefines it as yellow, my rendered output will be yellow - great!
The problem
When I need to reference variables that are only defined at other levels from my primary theme. For example:
// Primary theme:
// This fails since I haven't defined $brand-primary in my primary theme
$my-primary-theme-variable: $brand-primary;
The build now fails with an error saying primary-theme/src/scss/main.scss doesn't export content.
Workaround
I can work around this problem by copying the entire Bootstrap variables file through to my primary theme and changing variables as necessary, but I don't really want to do this.
Question
How does the SASS variable processor actually work? Is it possible for me to just change one of the Bootstrap variables in my theme without necessarily having to redefine the entire file?
This question is pretty similar.

It seems like you are using #include to import your SCSS try using #import instead – If this is just a typo in the question please let me know :-)
#import "./primary-variables.scss",
"/path/to/default/theme/main.scss"
;
I've added a few quick notes on the question you were referring to.
The important thing to know about the !default flag is that it takes effect at the point when it is used in a selector and does not re-define variables.
Sass does not look ahead when processing variables – it prints out the current value. In this example .class-1 will be red as the re-definition comes after it being used in the selector and .class-2 will be blue as there is no default flag.
$brand-color: red !default; // defined
.class-1 { background-color: $brand-color; } // red
$brand-color: blue; // re-defined
.class-2 { background-color: $brand-color; } // blue
Default flags will cause Sass to skip variable re-definition. In this example the result will be red as being defined first. The two following re-definitions are ignored because of the default flags.
$brand-color: red !default; // defined
$brand-color: blue !default; // ignored
$brand-color: green !default; // ignored
.class-1 { background-color: $brand-color; } // red
In this case all variables from from the config will be used – then variables from partial-1 if not defined in config and last partial-2 will define any variable not defined in the two others.
#import '_config.scss'; // definition
#import '_partial-1.scss'; // contains defaults
#import '_partial-2.scss'; // contains defaults
Hope it makes sense :-)
Import structure
// _default-theme.scss
#import '_default-variables.scss', '_bootstrap.scss';
// _primary-theme.scss
// primary variables will override defaults or use defaults if not defined
#import '_primary-variables.scss', '_default-theme.scss';
// style.scss
#import '_primary-theme.scss'; // or '_default-theme.scss'
Scope
In case your default and primary has content that is unique to each theme you could create a scoping mixin to handle what is compiled.
Here is a very rudimentary version:
// _scope.scss
$scope-context: null !default;
#function scope($scopes: null, $true: true, $false: false) {
#each $scope in $scope-context {
#if index($scopes, $scope) { #return $true }
}
#return $false;
}
#mixin scope($scopes: null) {
#if scope($scopes) or length($scopes) == 0 and not $scope-context {
#content;
}
}
How it works
The scope mixin takes a context argument and a content block #content. If the passed context matches a global variable ($scope-context) the content block get's rendered.
// _default-theme.scss
.class { content: 'Will show in both themes'; }
#include scope(default-theme){
.class { content: 'Will only show in the default theme'; }
}
#include scope(primary-theme){
.class { content: 'Will only show in the primary theme'; }
}
// can also be used as "if" function
.class {
content: scope(default-theme, 'Is default', 'Not default')
}
In your case define the $scope-context in both default and primary variables
// _default-variables.scss
$scope-context: default-theme !default;
// _primary-variables.scss
$scope-context: primary-theme;
... and add _scope.scss to the _default-theme.scss
// _default-theme.scss
#import '_default-variables.scss', '_bootstrap.scss', '_scope.scss';

The problem I found was that I was assuming things incorrectly about how SASS works.
When you define a variable declaration, the value of it is compiled at the time your write it. For example $my-var: $brand-primary would assign the current value of $brand-primary to $my-var at the time it is processed.
This means simply that I can't achieve what I wanted, which was to include a minimal variables file over the top of Bootstrap, because it would only update the variable itself, but not any other variables that reference that variable within Bootstrap.
The solution
It's not elegant, but duplicate the entire variable file for each theme and adjust them as required in each place.

Related

Feed theme variables to new Modules system

I work on styles for multi-tenant application for which the choice has been made to use SASS. Following the latest version guidelines, I started building base styles using #use and #forward. Now I want to theme this base with customers' colors and fonts variables to receive multiple stylesheets that can be served to different instances of our application.
If I was to use deprecated #import, I would go like this:
styles/customerA/index.scss
#import "customerA-variables";
#import "../base/index";
But with new rules I cannot find an easy way to simply feed theme specific variables to the base styles. I tried to use with keyword, but it turns out I need to define variables in a module, while I would rather encapsulate them in another module and import it.
styles/customerA/index.scss
#use "customerA-variables" as variables;
#use "../base/index" with (
$bgColor: variables.$bgColor,
$textColor: variables.$textColor
);
styles/base/_base-variables.scss
$bgColor: #eee !default;
$textColor: #333 !default;
styles/base/index.scss
/* HOW IT WORKS */
$bgColor: #eee !default;
$textColor: #333 !default;
/* HOW I WISH IT WORKED */
#use "./base-variables" as *;
/* LATER IN THE FILE */
body {
background-color: $bgColor;
color: $textColor;
}
In #use "../variables" as * scenario I get this error:
Error: This variable was not declared with !default in the #used module.
I'm looking for a working solution preferably without copy-pasting all theme variables inside with parenthesis.
First the error:
Error: This variable was not declared with !default in the #used module.
That error arises when you try to #use a module/file with configuration variables but the configuration variables are not set as !default variables in the module/file. So, SASS checks if you are passing a configuration which is not provided to be configurated for the module. That's additional security to you.
I am not quite sure if I did understand your example right, but it could be the way:
// styles/customerA/index.scss
#use "customerA-variables" as variables;
#use "../base/index" with (
$bgColor: variables.$bgColor,
$textColor: variables.$textColor
);
--> SASS ERROR because $bgColor and/or $textColor
--> in #used module/file are not set as defaults, i.e.:
--> $bgColor: green;
--> must be: $bgColor: green !default;
So, you may check the module if the variables are all set to defaults and not overwritten by non-default values.
**SECOND: usage of #use:
The new rule #use indeed is really confusing ... in your example that leads to doubled code: once when you set the custom vars in customerA-variables.scss and then when you repeat that variables when you #use the module/file in styles/customerA/index.scss (see your second code example).
A good way to avoid that doubled code is to prepare a configuration file with the settings for the individual customer and THAN #use the configuration file (not the wanted module/file direct).
Example:
// ###
// ### module/file: ../base/index.scss
$bgColor: #eee !default;
$textColor: #333 !default;
body {
background-color: $bgColor;
color: $textColor;
}
// ###
// ### customer configuration file: configModuleCustomerA.scss
#forward "../base/index" with (
$bgColor: red,
$textColor: blue
);
// ###
// ### use that configuration file
// ### to your main file: styles/customerA/index.scss
#use "...path.../configModuleCustomerA" as *;
// Note:
// 1. that writes the configurated css
// 2. and it provides the variables for additional use to THIS file
// see going on this file below ...
.additionalElement {
background: $bgColor;
color: $textColor;
}
// ###
// ### ---> that compiles to CSS:
body {
background-color: red;
color: blue;
}
.additionalElement {
background: red;
color: blue;
}
NOTE: there is an additional TRICK/EFFECT you should know.
YOU ONLY NEED TO SET THE VARIABLES ONCE ... just the time when you config the module/file. And as you do it in a/the module config file the variables you set there are part of you project configuration!
So, if you need the SAME variables of a module a second/third/... time (i.e. in additional partials files) you #use that configuration file in any file where you need it/them. Don't worry: the css code is only compiled ONCE to your CSS: the first time you #use the module.
BUT HEAD UP IN YOUR CASE:
But if you want to #use a module/file with different configurations as in your case you have to compile it into two DIFFERENT CSS files. One module with two or more different configurations loaded to the same CSS is blocked by SASS. In that case you need to split the css to different customer css files which all uses different module configuration files.

Dynamic import style and set scss variable value

I have an admin template in which I would like user to provide option to switch between different theme color such as dark/light theme along with setting variable values, but I'm unable to conditionally import style files.
#if $light-theme {
#import "./_metronic/_assets/sass/themes/layout/aside/light.scss";
$primary: green;
} #else {
#import "./_metronic/_assets/sass/themes/layout/aside/dark.scss";
$primary: blue;
}
Getting error: Import directives may not be used within control directives or mixins.
Also it is a very long wished item to sass ... as the error message returns: that is not possible. (https://github.com/sass/sass/issues/279)
But what you can do is setting a theme variable and import your theme files. Every theme file has an conditional output based on the theme variable:
$theme: 'light-theme';
#import 'path/dark-theme';
#import 'path/light-theme';
// EVEREY THEME FILE WITH STRUCTURE:
#if( $theme == 'this-theme' ){
/*
here goes your theme variables or
if whanted your theme code in the file
*/
}

Creating SASS override for white labelled application

I'm running a white label application where many brands are using a set of SASS variables. But over the time, it happens that additional variables needs to be added for a given breakpoint in order to allow more flexibility. Thus, in order to keep backward compatibility, I assign the current value to the newly created variable ($test--large). For the brand, to be able to customize style for large breakpoint.
The issue is that it doesn't work the way i'd expect it to work. If at brand level, I change $test, i'd like $test--large to also have the new value assignment.
Hope it makes sense, if not what would you suggest. Knowing that we have many brands and it would be painful to redefine all the new variables.
$test: blue !default;
$test--large: $test !default; // new variable
$test: green; // Override from brand
.text {
color: $test;
// Newly added piece
#include breakpoint(large) {
color: $test--large; //result will be blue but i'd expect it to be green
}
}
`
It looks like when you assign $test to $test--large, the variable is being passed by value, not reference. Hence when you reassign $test to green, you need to also reassign $test--large, e.g. like this:
$test--large: $test
or
$test--large: green
I wanted to avoid this solution because it will require me to change it in all my project.
The variables override need to happen before and not after the !default values
I'm still wondering if it isn't possible without interchanging the variable import order
$test: green; // Override from brand
$test: blue !default;
$test--large: $test !default; // new variable
.text {
color: $test;
// Newly added piece
#include breakpoint(large) {
color: $test--large; //result will be green as expected
}
}

What is best fit to use for theming variables or mixins in sass

I am creating a UI library in which I want to provide the mechanism to theme all the UI components like button, cards, slider and all. I am confused between variables and mixins.
One way is to provide the no. of variables that user can update and based on that variables component classes will be derived. The same concept is used in the materialzecss library. And user will use like
//variables that are used to create component css classes
$primary : "blue";
$btn-primary :"green";
//then include the ui library
#import "_ui-variables";
#import "ui-library";
_ui-variables.scss
$primary : "red" !default;
$btn-primary: $primary !default;
// and other variables
and the _btn.scss will be like
.btn {
// other rule sets
color:$btn-primary;
}
Other way could be to use mixins. There will be a theme file for every component that will contain the theme mixin for that component and at the library level, there will be theme mixin that will include all the mixin of the individual component. As the angular-material has done
_btn.scss
#import "_btn-theme.scss";
.btn {
// some rules
}
_btn-theme.scss
#mixin btn-theme($theme) {
// if user has added the btn-primary then use btn-primary otherwise primary
#if map-has-key($theme,btn-primary) {
$btn-primary : map-get($theme,primary);
} #else {
$btn-primary : map-get($theme,primary);
}
.btn {
color:$btn-primary;
}
}
and the ui-library.scss
#import "_btn.scss";
#import "_card.scss";
#mixin ui-theme($theme) {
#include btn-theme($theme);
#include card-theme($theme); // include all component theme
}
and the consumer will call this as
consumer-theme.scss
#import "ui-library";
$theme :(primary:"blue",accent:"yellow");
#include ui-theme($theme);
What are the pros and cons of these approaches? Is there any other way to do this?
If you can use CSS custom properties (CSS variables) that would be really easy. You would just need to add a class to the body change your all your variables at once. So you just need a default theme and then just some classes changing your theme.
I have a small example in one of my project, if you click on "invert theme" it will change the page theme to invert: https://vinceumo.github.io/atomic-bulldog-style-guide-demo/styleguide/section-organisms.html#kssref-organisms-accessibility-settings
The issue with CSS custom properties is that not every brother support it yet :/
https://caniuse.com/#feat=css-variables
Otherwise, I would highly recommend using sass maps. It is easier to maintain when you have few themes, and you can quickly generate your components using #each loop
For example, if you want to generate background color classes:
$color-themes: (
primary:
(
base: #4c5c8c,
dark: darken(#4c5c8c, 15%),
light: lighten(#4c5c8c, 15%),
transparent: transparentize(#4c5c8c, 0.5),
contrast: #ffffff
),
secondary:
(
base: #212529,
dark: darken(#212529, 15%),
light: lighten(#212529, 15%),
transparent: transparentize(#212529, 0.5),
contrast: #ffffff
)
}
#each $name, $theme in $color-themes {
.has-bg-#{$name} {
background-color: map-get($name, base);
color: map-get($name, contrast);
}
}
So here we will get two new classes .has-bg-primary, .has-bg-secondary
If you add new entries to your map it will automatically generate new classes :)
I have created a Scss boilerplate using CSS custom properties (This one can be disabled) with Sass variables. It is optimized for themes creation. Most components are linked to variables (using map). Check it out https://github.com/vinceumo/atomic-bulldog
Variables are going to be your initial best bet.
Creating a theme story isn't something you should rush through, but rather take the time to integrate solid, well thought variables for a base set of colors. After that, you can extend them with things like lighten(), darken(), and other tools built into SASS.
Then, use that base set of variables to establish component specific variables to scale the theme story as needed.

Overriding Sass mixin in view template dynamically

I've defined a Sass mixin and used it in a class styling. I'm using the class in template. I need to decide the color on runtime. So I'm trying to override mixin. But its showing in #ff0000 the color I defined in mixin initially.
For some reason I can't use an extra class, What could be the best solution in this scenerio.
My stylesheet app.scss
#mixin mx-color {
color: #ff0000;
}
.my-color {
#include mx-color;
}
Angular template view app.html.haml
:css
#mixin mx-color {
color: {{custom_color}}; // custom_color contains hex-color to replace
}
%body
%p.my-color
This text must show-up in custom color but its showing the default color.
Please check the compiled HTML/CSS output. I don’t think it is possible to just overwrite a mixin and expect the SASS blocks where the mixin was used to update automatically.

Resources