Skip to content

Instantly share code, notes, and snippets.

@johanlef
Last active September 6, 2024 10:03
Show Gist options
  • Save johanlef/518a511b2b2f6b96c4f429b3af2f169a to your computer and use it in GitHub Desktop.
Save johanlef/518a511b2b2f6b96c4f429b3af2f169a to your computer and use it in GitHub Desktop.
Use CSS custom properties (--var) with bootstrap 4 (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>';
@danwalker-caci
Copy link

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
Copy link
Author

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
Copy link

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
Copy link
Author

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

@danwalker-caci
Copy link

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
Copy link
Author

No problem, enjoy!

@RaphaelMcledger
Copy link

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);

@CobusKruger
Copy link

Hi, and thank you for sharing this. I have a question about a build warning I get:

DEPRECATION WARNING: $lightness: Passing a number without unit % (0) is deprecated.

To preserve current behavior: $lightness * 1%

    ╷
118 │     @return hsla($h, $s, $l, $a);
    │             ^^^^^^^^^^^^^^^^^^^^
    ╵
    styles\bootstrap-overrides\_functions.scss 118:13  render-hsla()

I suppose I can ignore it for the moment, but do you know how to get around it? The $lightness * 1% trick doesn't work with the CSS variables.

@MostafaTaghipour
Copy link

can you update it to compatible Bootsrap version 5, please?

@dinandmentink
Copy link

Subscribing to this for a bootstrap 5 version. I'm working on this myself atm, but am stuck on the luminance function.

@johanlef
Copy link
Author

@MostafaTaghipour I'm not (yet) working with Bootstrap 5, so no real urgency to update the code atm.

@dinandmentink Where exactly are you stuck with "the luminance" function? Is this a BS5 built-in function?

@CobusKruger Could your build error be related to the default values on lines 57-59 in _functions-override.scss?

@dinandmentink
Copy link

@johanlef Thanks for your response. The thing is that bootstrap 5 has a color-contrast method (which can be overridden) similar to the yiq from bootstrap 4, which uses a contrast-ratio method that relies heavily on luminance.

However. When trying to override luminance in some way that uses css variables (and not just a fixed value) then there is 1 other location that uses contrast-ratio in an @if statement (thus, a value needs to be available in compile time) which is _alerts.scss. It could be bootstrap 5 can't be tricked into using css variables the same way bootstrap 4 could. Except maybe by overriding _alerts.scss.

I hope I'm making sense. I have no experience in css variables in scss context.

@johanlef
Copy link
Author

johanlef commented Jul 5, 2021

@dinandmentink Thanks for clarifying! I don't have experience with BS5 yet, so currently I can't help you out, sorry! I'll have to look into it in detail, but I'm afraid that wouldn't be anytime soon. Feel free to share if you have a breakthrough in your attempts!

@aj-dev-smith
Copy link

@dinandmentink Could you possibly post any additional overrides that are specifically needed for bootstrap 5 you've found so far? We are also going through a similar exercise at my company.

@dinandmentink
Copy link

dinandmentink commented Jul 14, 2021

So. This is what I had for function-voerrides.scss.

@function opacity($color) {
  @return 0.5; // Determine based on css ?
}

@function luminance($color) {
  @return 50; // Only relevant as fallback for alerts
}

@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-contrast(
  $color,
  $color-contrast-dark: $color-contrast-dark,
  $color-contrast-light: $color-contrast-light,
  $min-contrast-ratio: $min-contrast-ratio
) {
  @if (is-color($color)) {
    $r: red($color);
    $g: green($color);
    $b: blue($color);

    $yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;

    @if ($yiq >= $min-contrast-ratio) {
      @return $color-contrast-dark;
    } @else {
      @return $color-contrast-light;
    }
  } @else {
    $c: to-hsl($color);
    $l: map-get($c, l);

    $th: $min-contrast-ratio / 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);
  }
}

And then I had this in a bootstrap.scss referenced from my main entrypoint.

//
// Bootstrap
//
// Custom import of bootstrap to allow theming behaviour
//

//
// Functions
// Import bootstrap functions first and then override them
//
@import "~bootstrap/scss/functions";
@import "functions-override";

//
// Variables
// Import bootstrap variables and then override them.
//
@import "~bootstrap/scss/variables";
@import "variables";

//
// Import
// Import bootstrap utilities and then amend them.
//
@import "~bootstrap/scss/utilities";
@import "utilities";

//
// Bootstrap
// Import the rest of bootstrap as usual
//

@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/root";
@import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type";
@import "~bootstrap/scss/images";
@import "~bootstrap/scss/containers";
@import "~bootstrap/scss/grid";
@import "~bootstrap/scss/tables";
@import "~bootstrap/scss/forms";
@import "~bootstrap/scss/buttons";
@import "~bootstrap/scss/transitions";
@import "~bootstrap/scss/dropdown";
@import "~bootstrap/scss/button-group";
@import "~bootstrap/scss/nav";
@import "~bootstrap/scss/navbar";
@import "~bootstrap/scss/card";
@import "~bootstrap/scss/accordion";
@import "~bootstrap/scss/breadcrumb";
@import "~bootstrap/scss/pagination";
@import "~bootstrap/scss/badge";
@import "~bootstrap/scss/alert";
@import "~bootstrap/scss/progress";
@import "~bootstrap/scss/list-group";
@import "~bootstrap/scss/close";
@import "~bootstrap/scss/toasts";
@import "~bootstrap/scss/modal";
@import "~bootstrap/scss/tooltip";
@import "~bootstrap/scss/popover";
@import "~bootstrap/scss/carousel";
@import "~bootstrap/scss/spinners";
@import "~bootstrap/scss/offcanvas";
@import "~bootstrap/scss/helpers";
@import "~bootstrap/scss/utilities/api";

This should compile, but there are still some issues. Obviously opacity and luminance ar now hardcoded and should be based on css variables / calc. It leads to weird on-hover / gradient / fade colors. However, if we change luminance to be anything other than a value then alerts.scss will crash because of the line @if (contrast-ratio($alert-background, $alert-color) < $min-contrast-ratio) { which references contrast-ratio which references luminance which then has no value available in compiletime, only in runtime.

I hope i'm making sense. Good luck. Please share if you have any succes.

Edit: please note that this script is also still using a modified version of color-yiq, which works different than the contrast-ratio and color-contrast functions in bootstrap 5.

Edit 2: this was written pre-bootstrap 5.1 which includes more css variables. I highly recommend checking out bootstrap 5.1 and the next few comments first.

@jipis
Copy link

jipis commented Aug 12, 2021

@dinandmentink, I was banging on your code above and just COULD not get it to work. I kept getting an error that to-rgb won't work on (eg) var(--blue). Yay? Poke, poke, poke. Oh, I'm on bootstrap 5.1, which was only released a few days ago. And your post is a few weeks old now. And, for funsies, it seems that the update to bootstrap 5.1 adds a to-rgb function that you (obviously) hadn't accounted for in your older code. For now, I'm pinned to ~5.0.2 for bootstrap which makes (with warnings) your code above happy. If I get a round tuit at some point soon (and can figure out how to do more than just-barely read scss functions), I'll see if I can't write a replacement for to-rgb mimicking the to-hsl you've got above.

@dinandmentink
Copy link

dinandmentink commented Aug 12, 2021

Ah. This code was written pre-5.1, yes.

In bootstrap 5.1 more utility classes (like .bg-primary and .text-secondary) are now using css variables. I have managed to get full styling based on css variables using bootstrap 5.1 for my specific usecase by:

  • changing as much as possible (just see if it breaks) variables to reference css variables in variables.scss
  • include a theme.scss at the end of my app.scss entrypoint which overrides a few items (that are used by my app) based on css variables.

It's a bit hacky, and quite bootstrap dependent but it works as much as I need it to. I expect I will have to manually check themeing still works on bootstrap upgrades.

So for now I have solved my usecase, but will keep following this thread because a solution that doesn't require a hacky theme.scss and allows themeing everything (not just the items I bothered to included based on what I use) might be better.

I have put the gist of it here: https://gist.github.com/dinandmentink/4c3453bb3f3370c889ec84960d363237, it's very specific to the elements I need on my website, might help someone else.

Edit: I would recommend checking out bootstrap 5.1, I got it to work without the magic mixins posted above.

@jipis
Copy link

jipis commented Aug 12, 2021

Nah, no good. My variables file has, for example

$black: var(--black);
$blue: var(--blue);
$indigo: var(--indigo);
$purple: var(--purple);
$pink: var(--pink);

With the css variables being set on :root and :root.theme-x differently. Our theme switcher just changes the class at the root of the page, variables get new values, theme changed. But, it relies on css variables for runtime value changes. It's not the only way to do this, sure; but it is the road we've already headed down.

With bootstrap/scss not really making things easy with the update to bs5 (worked great for us with react-bootstrap for bs4), I'm thinking it's time to find a new solution. :-/

@R-Iqbal
Copy link

R-Iqbal commented Jan 5, 2022

Hey, can you point me in the right direction to understand exactly what primary-h, primary-s and primary-l are doing and how the values can be set?

@johanlef
Copy link
Author

johanlef commented Jan 6, 2022

@R-Iqbal primary-h, primary-s and primary-l are the hue, saturation and lightness of the color I named primary, e.g. the main color of your brand. The values can be set manually (hardcoded) as shown in the html file, or generated from javascript as seen in the jsx file where I wrote the function ApplyBranding to convert color values to css 'custom properties'.

Example:

<App>
  <ApplyBranding colors={{ primary: 'hsl(30, 40%, 50%)' }} />
  {/* you can use RGB, HSL and HEX values, no html color names */}
</App>

… will generate this style element in your html …

<style>
  :root {
    --primary: hsl(30, 40%, 50%);
    --primary-h: 30;
    --primary-s: 40%;
    --primary-l: 50%;
  }
</style>

… which is necessary to make the sass code work (to calculate the correct darken, lighten etc values).

→ So you can set the style values manually, or generate them with javascript, which was necessary in my project, since the colors were set externally, which was the main reason of creating this gist to start with.

I hope this helps?

@R-Iqbal
Copy link

R-Iqbal commented Jan 6, 2022

@johanlef Gotcha that makes a lot of sense. Is there any reason why we need to store the hue, saturation and light in their own variables? Is there a reason why they are not just derived from the call to hsl?

@johanlef
Copy link
Author

johanlef commented Jan 7, 2022

@R-Iqbal They are needed as separate css-variables (not sass-variables!) because I make use of the css calc() function. Maybe some optimisations are possible 🙂

The sass-functions above are modified to be able to accept css-custom-properties (css variables), in essence they generate css-code to calculate the new colors on runtime instead of during sass-preprocessing. This enables you to update colors programmatically in-app, injected by js or other sources like user input or custom stylesheets.

@mielp
Copy link

mielp commented Feb 4, 2022

Note for Bootstrap 4 users, if you want correctly themed alerts, list group items, and other things relying on Bootstrap's internal theme-color-level function, you will need to override that function as well. Here is my take on it, free to use:

@function theme-color-level($color-name: "primary", $level: 0) {
  $color: theme-color($color-name);
  @if ($level == 0) {
    @return $color;
  }

  $amount: $theme-color-interval * abs($level) / 100%;
  $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 ($level > 0) {
    // Darken -X%: L = L * (1 - X)
    $rl: calc((#{$l} * #{1 - $amount}));
    @return render-hsla($h, $s, $rl, $a);
  }
  @if ($level < 0) {
    // Ligthen +X%: L = L + X * (100 - L)
    $rl: calc(#{$l} + #{$amount} * (100% - #{$l}));
    @return render-hsla($h, $s, $rl, $a);
  }
}

@reshmamarla
Copy link

Thank you for the solution.
i am facing some issues while integrating, getting the below error


$c: str-slice($c, $startPos + 1, -2); // 3 or 4 comma-separated vomplex values
                     ^
 Invalid null operation: "null plus 1"

i have hardcoded the colour value in my HTML
<style> :root{ --primary: hsl(30, 40%, 50%); --primary-h: 30; --primary-s: 40%; --primary-l: 50%; } </style>

Not sure what i am missing here.Any help with this is appreciated. Thank you

@johanlef
Copy link
Author

@reshmamarla You could try to log the value of $c on which $startPos is calculated using the @debug sass function.

Your error was reported before, but people seemed to have found a way around it isolate a problem outside my code.

@rafeehcp
Copy link

@reshmamarla did you resolve the issue?

@rafeehcp
Copy link

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!

@danwalker-caci Could you please describe the changes you made?

@Tristan10
Copy link

Tristan10 commented Dec 7, 2022

Fairly old thread, but did anyone have or solve issues related to the color-yiq function in functions-override.scss ? That function seems to be the root cause of the button shadows (when focussed) not working correctly.

I'm using bootstrap 4.6.2

@JohnnyTWA
Copy link

Hello,

Thanks for creating the function override. Is this available as an npm package? If not would you mind if I create one, you'll obviously be credited and include links back to the original function.

@johanlef
Copy link
Author

@JohnnyTWA Please do, I have been neglecting comments and updates on this for too long. I welcome you to make this an npm package so more people can create nice things. Thanks a lot!

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