Skip to content

Instantly share code, notes, and snippets.

@johanlef

johanlef/README.md

Last active Feb 25, 2021
Embed
What would you like to do?
Use CSS custom properties (--var) with bootstrap SCSS

The file _functions-override.scss contains the custom functions to handle color conversions within sass and bootstrap.

Bootstrap does not like its sass variables set to css custom properties, e.g. var(--primary). If you use the code snippets below, you can do so, under some conditions.

In the most basic case, you should provide your color variables using the hsl format.

If you insert this using javascript, you can use the script apply-colors.jsx to let js handle the conversion from hex or rgb to hsl.

Reference the main.scss file to import the files in the correct order.

@function is-color($color) {
@if (type-of($color) == color) {
@return true;
}
@return false;
}
@function count-occurrences($string, $search) {
$searchIndex: str-index($string, $search);
$searchCount: 0;
@while $searchIndex {
$searchCount: $searchCount + 1;
$string: str-slice($string, $searchIndex + 1);
$searchIndex: str-index($string, $search);
}
@return $searchCount;
}
@function str-is-between($string, $first, $last) {
$firstCount: count-occurrences($string, $first);
$lastCount: count-occurrences($string, $last);
@return $firstCount == $lastCount;
}
@function recursive-color($color, $index: 0) {
$indices: (
0: h,
1: s,
2: l,
3: a
);
// find end of part
$end: str-index($color, ',');
@while ($end and not str-is-between(str-slice($color, 0, $end - 1), '(', ')')) {
$newEnd: str-index(str-slice($color, $end + 1), ',');
@if (not $newEnd) {
$newEnd: 0;
}
$end: 2 + $end + $newEnd;
}
@if ($end) {
$part: str-slice($color, 0, $end - 1);
$value: map-merge(
(
map-get($indices, $index): $part
),
recursive-color(str-slice($color, $end + 1), $index + 1)
);
@return $value;
}
@return ();
}
@function to-hsl($color) {
$c: inspect($color);
$h: 0;
$s: 0;
$l: 0;
$a: 1;
@if (is-color($color)) {
// std color
$h: hue($color);
$s: saturation($color);
$l: lightness($color);
$a: alpha($color);
@return (h: $h, s: $s, l: $l, a: $a);
}
@if (str-slice($c, 0, 3) == 'var') {
// var(--color)
$commaPos: str-index($c, ',');
$end: -2;
@if ($commaPos) {
$end: $commaPos - 1;
}
$var: str-slice($c, 7, $end);
$h: var(--#{$var}-h);
$s: var(--#{$var}-s);
$l: var(--#{$var}-l);
$a: var(--#{$var}-a, 1);
@return (h: $h, s: $s, l: $l, a: $a);
}
@if ($c == '0') {
@return (h: $h, s: $s, l: $l, a: $a);
}
// color is (maybe complex) calculated color
// e.g.: hsla(calc((var(--white-h) + var(--primary-h)) / 2), calc((var(--white-s) + var(--primary-s)) / 2), calc((var(--white-l) + var(--primary-l)) / 2), calc((var(--white-a, 1) + var(--primary-a, 1)) / 2)), hsla(calc((var(--white-h) + var(--primary-h)) / 2), calc((var(--white-s) + var(--primary-s)) / 2), calc((var(--white-l) + var(--primary-l)) / 2), calc((var(--white-a, 1) + var(--primary-a, 1)) / 2))
$startPos: str-index($c, '(');
$c: str-slice($c, $startPos + 1, -2); // 3 or 4 comma-separated vomplex values
@return recursive-color($c);
// $hEnd: str-index($c, ',');
// @if ($hEnd) {
// $h: str-slice($c, 0, $hEnd - 1);
// $c: str-slice($c, $hEnd + 1);
// $sEnd: str-index($c, ',');
// @if ($hEnd) {
// $h: str-slice($c, 0, $hEnd - 1);
// $c: str-slice($c, $hEnd + 1);
// $sEnd: str-index($c, ',');
// }
// }
// @return (h: $h, s: $s, l: $l, a: $a);
}
@function render-hsla($h, $s, $l, $a: 1) {
@return hsla($h, $s, $l, $a);
}
@function lighten($color, $amount) {
@if (is-color($color)) {
@return scale-color($color: $color, $lightness: $amount);
}
$c: to-hsl($color);
$h: map-get($c, h);
$s: map-get($c, s);
$l: map-get($c, l);
$a: map-get($c, a);
@return render-hsla($h, $s, calc(#{$l} + #{$amount}), $a);
}
@function darken($color, $amount) {
@return lighten($color, $amount * -1);
}
@function rgba($red, $green, $blue: false, $alpha: false) {
$color: $red;
@if (not $blue and not $alpha) {
$alpha: $green;
$color: $red;
}
$c: to-hsl($color);
$h: map-get($c, h);
$s: map-get($c, s);
$l: map-get($c, l);
@return render-hsla($h, $s, $l, $alpha);
}
@function rgb($red, $green, $blue) {
@return rgba($red, $green, $blue, 1);
}
@function mix($color-1, $color-2, $weight: 50%) {
$c1: to-hsl($color-1);
$c2: to-hsl($color-2);
$h1: map-get($c1, h);
$s1: map-get($c1, s);
$l1: map-get($c1, l);
$a1: map-get($c1, a);
$h2: map-get($c2, h);
$s2: map-get($c2, s);
$l2: map-get($c2, l);
$a2: map-get($c2, a);
$h: calc((#{$h1} + #{$h2}) / 2);
$s: calc((#{$s1} + #{$s2}) / 2);
$l: calc((#{$l1} + #{$l2}) / 2);
$a: calc((#{$a1} + #{$a2}) / 2);
@return render-hsla($h, $s, $l, $a);
}
@function fade-in($color, $amount) {
$c: to-hsl($color);
$h: map-get($c, h);
$s: map-get($c, s);
$l: map-get($c, l);
$a: map-get($c, a);
@if (not $a) {
$a: 1;
}
@return render-hsla($h, $s, $l, $a + $amount);
}
@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
@if (is-color($color)) {
$r: red($color);
$g: green($color);
$b: blue($color);
$yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
@if ($yiq >= $yiq-contrasted-threshold) {
@return $dark;
} @else {
@return $light;
}
} @else {
$c: to-hsl($color);
$l: map-get($c, l);
$th: $yiq-contrasted-threshold / 2.56; // convert hex to dec
$lightness: calc(-100 * calc(#{$l} - #{$th * 1%}));
// ignoring hue and saturation, just a light or dark gray
@return render-hsla(0, 0%, $lightness, 1);
}
}
// This code generates correct css custom properties
// from any color code (no named color yet)
import React from 'react'
import identity from 'lodash/identity'
import map from 'lodash/map'
import trim from 'lodash/trim'
const printCss = (suffix = '', convert = identity) => {
return (value, property) => `--${property}${suffix ? '-' + suffix : ''}: ${convert(value)};`
}
const rgbToHsl = (red, green, blue) => {
const r = Number(trim(red)) / 255
const g = Number(trim(green)) / 255
const b = Number(trim(blue)) / 255
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
let h,
s,
l = (max + min) / 2
if (max === min) {
h = s = 0 // achromatic
} else {
const d = max - min
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0)
break
case g:
h = (b - r) / d + 2
break
case b:
h = (r - g) / d + 4
break
}
h /= 6
}
h = Math.round(360 * h)
s = Math.round(100 * s)
l = Math.round(100 * l)
return [h, s, l]
}
// from @josh3736 | https://stackoverflow.com/a/3732187
const colorToHsl = color => {
if (color.slice(0, 1) === '#') {
if (color.length === 4) {
const r = parseInt(color.substr(1, 1) + color.substr(1, 1), 16)
const g = parseInt(color.substr(2, 1) + color.substr(2, 1), 16)
const b = parseInt(color.substr(3, 1) + color.substr(3, 1), 16)
return rgbToHsl(r, g, b)
} else {
const r = parseInt(color.substr(1, 2), 16)
const g = parseInt(color.substr(3, 2), 16)
const b = parseInt(color.substr(5, 2), 16)
return rgbToHsl(r, g, b)
}
} else if (color.slice(0, 4) === 'rgba') {
const [r, g, b] = color.slice(5, -1).split(',')
return rgbToHsl(r, g, b).slice(0, 3)
} else if (color.slice(0, 3) === 'rgb') {
const [r, g, b] = color.slice(4, -1).split(',')
return rgbToHsl(r, g, b)
} else if (color.slice(0, 4) === 'hsla') {
return color.slice(5, -1).split(',').slice(0, 3)
} else if (color.slice(0, 3) === 'hsl') {
return color.slice(4, -1).split(',')
} else {
// named color values are not yet supported
console.error('Named color values are not supported in the config. Convert it manually using this chart: https://htmlcolorcodes.com/color-names/')
return [0, 0, 16] // defaults to dark gray
}
}
export const ApplyBranding = ({ colors }) => {
if (colors) {
return (
<style>
{':root {'}
{colors &&
map(
colors,
printCss('', color => {
const hsl = colorToHsl(color)
return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`
})
)}
{colors &&
map(
colors,
printCss('h', color => {
const hsl = colorToHsl(color)
return hsl[0]
})
)}
{colors &&
map(
colors,
printCss('s', color => {
const hsl = colorToHsl(color)
return `${hsl[1]}%`
})
)}
{colors &&
map(
colors,
printCss('l', color => {
const hsl = colorToHsl(color)
return `${hsl[2]}%`
})
)}
})}
</style>
)
} else return null
}
// application (React)
<App>
<ApplyBranding colors={{ primary: 'hsl(30, 40%, 50%)', secondary: 'rgb(192, 144, 32)', light: '#FFEEAA' }} />
{/* App components */}
</App>
<!DOCTYPE html>
<html>
<head>
<style>
:root {
/* Provide your colors in hsl format! */
--primary: hsl(30, 40%, 50%);
--primary-h: 30;
--primary-s: 40%;
--primary-l: 50%;
/* See below how to generate this with javascript from any color code! */
}
</style>
</head>
<body />
</html>
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/mixins';
// override bootstrap functions to comply with --vars
@import 'functions-override';
// define static bootstrap variables here
$border-radius: 1em;
// finally import bootstrap (or a subset)
// do not import ~bootstrap/scss/bootstrap
// because it will override our own color-yiq
@import '~bootstrap/scss/variables';
@import '~bootstrap/scss/<module>';
@edouardruiz

This comment has been minimized.

Copy link

@edouardruiz edouardruiz commented Aug 3, 2020

Many thanks for this gist, do you know if it would be easy to enhance the mix function to take weight into account?

@johanlef

This comment has been minimized.

Copy link
Owner Author

@johanlef johanlef commented Aug 4, 2020

I think you could try something like this on line 167 of _functions_override.scss (if $weight is a number between 0..1):

$h: calc(calc(#{$weight} * #{$h1}) + calc(#{1 - $weight} * #{$h2}));

Also, the hue calculation in mix might need refining: if abs(h1 - h2) > 180, you will get the complementary color instead of the one in between.

@soren121

This comment has been minimized.

Copy link

@soren121 soren121 commented Sep 9, 2020

Thank you for this! I have a small suggestion:

The Dart-Sass compiler throws on line 119 of _functions-override.scss, because it seems to require named arguments in lieu of null positional arguments. I changed this line to @return scale-color($color: $color, $lightness: $amount); and it compiled correctly.

This was the error I got, if you're curious:

SassError: Only one positional argument is allowed. All other arguments must be passed by name.
    ╷
119 │         @return scale-color($color, null, null, null, null, $amount, null);
    │                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ╵
  src/components/layout/_functions-override.scss 119:17  lighten()
@johanlef

This comment has been minimized.

Copy link
Owner Author

@johanlef johanlef commented Sep 11, 2020

@soren121 Thanks, good to know this works too!

@evpaassen

This comment has been minimized.

Copy link

@evpaassen evpaassen commented Sep 21, 2020

@johanlef Thanks for your this awesome piece of code! I think it's a really creative way of solving the runtime theming issue and it works quite well.

I found an issue, preventing me to use it with Bootstrap 3, though. The rgba() function doesn't seem to use the $green and $blue arguments when passed 4 values, so it doesn't actually work with real rgba colors. I fixed it this way:

@function rgba($red, $green, $blue: false, $alpha: false) {
  $color: $red;

  @if (not $blue and not $alpha) {
    $alpha: $green;
  } @else {
    $color: change-color(#000, $red: $red, $green: $green, $blue: $blue);
  }

  $c: to-hsl($color);
  $h: map-get($c, h);
  $s: map-get($c, s);
  $l: map-get($c, l);
  @return render-hsla($h, $s, $l, $alpha);
}

I'd like to use parts of this gist in my project, is that OK? And if so, under what license?

@johanlef

This comment has been minimized.

Copy link
Owner Author

@johanlef johanlef commented Sep 22, 2020

@evpaassen This Gist is free to use, no strings attached! I only tested my code with Bootstrap 4, maybe that's why you had issues?

@evpaassen

This comment has been minimized.

Copy link

@evpaassen evpaassen commented Sep 22, 2020

@evpaassen This Gist is free to use, no strings attached! I only tested my code with Bootstrap 4, maybe that's why you had issues?

Thanks!

I only tested my code with Bootstrap 4, maybe that's why you had issues?

Bootstrap 3 defines some color variables in rgba format, like here:

$dropdown-border:                rgba(0, 0, 0, .15) !default;

Your current rgba() replacement doesn't support this, as it doesn't do anything with the values for $green and $blue. So I added the else block in my previous comment, which fixes that by rewriting it to a hex color. Feel free to include it in your gist if you want. ;-)

@KSundman

This comment has been minimized.

Copy link

@KSundman KSundman commented Dec 18, 2020

Dang this is nice. Thanks for posting.

Quick question: I'm seeing some issues with some of bootstrap's hover styles after trying this out. My button, nav-link, and nav-tab hovers all started behaving strangely (losing background color, text color not changing). I suppose it's possible I screwed something else up at the same time but---- any thoughts? Thanks again!

@johanlef

This comment has been minimized.

Copy link
Owner Author

@johanlef johanlef commented Dec 19, 2020

Dang this is nice. Thanks for posting.

Quick question: I'm seeing some issues with some of bootstrap's hover styles after trying this out. My button, nav-link, and nav-tab hovers all started behaving strangely (losing background color, text color not changing). I suppose it's possible I screwed something else up at the same time but---- any thoughts? Thanks again!

@KSundman You're welcome! The color-yiq is not perfect, so that may be the cause, also it works best with HSL color values. I have used this in some projects, the only issues I had was that some hover-colors rendered either black or white incorrectly due to the custom color-yiq function which is not (yet) optimised. If you supply a code sample, I can look into it some day.

@danwalker-caci

This comment has been minimized.

Copy link

@danwalker-caci danwalker-caci commented Feb 15, 2021

This is a great resource but I am having issues trying to use it. I receive an error on line 96 of the overrides: $c: str-slice($c, $startPos + 1, -2);

It errors stating that 1 is not a number which I am guessing it is evaluating $c as 1 or something. Can't seem to figure this out. Does having HSL values really work better for the color perhaps that might be it but seems odd. Thanks!!

@johanlef

This comment has been minimized.

Copy link
Owner Author

@johanlef johanlef commented Feb 15, 2021

All colors are converted to HSL to be able to easily change lightness etc on the --vars. The code should work for rgba and hsla values, not for named colors. You can check out what the values of the variables are at the error by using the @debug sass function. It is also possible we are encountering an untested or unforeseen use case 😄.

@danwalker-caci

This comment has been minimized.

Copy link

@danwalker-caci danwalker-caci commented Feb 15, 2021

Yes, the error hits when trying to resolve "hsla(var(--black-h), var(--black-s), var(--black-l), 0.2)" but it seems like that should work.

@johanlef

This comment has been minimized.

Copy link
Owner Author

@johanlef johanlef commented Feb 15, 2021

Seems right indeed. Maybe one of the --black-*s is not correctly parsed.

@danwalker-caci

This comment has been minimized.

Copy link

@danwalker-caci danwalker-caci commented Feb 15, 2021

Turns out that the function was working fine but some sass functions needed some tweaking and there were a plethora of bad sass variables in the vendor scss files that I was including. Wow!! Thanks again!

@johanlef

This comment has been minimized.

Copy link
Owner Author

@johanlef johanlef commented Feb 16, 2021

No problem, enjoy!

@RaphaelMcledger

This comment has been minimized.

Copy link

@RaphaelMcledger RaphaelMcledger commented Feb 25, 2021

Hi, thank's u for your code but i got this error :
Invalid null operation: "null plus 1".
on line 108 of src/assets/scss/functions-override.scss, in function to-hsl
from line 156 of src/assets/scss/functions-override.scss, in function rgba

When i run this code of css : rgba(4, 106, 243, 0.5);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment