Skip to content

Instantly share code, notes, and snippets.

@johanlef johanlef/README.md

Last active Aug 4, 2020
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, null, null, null, null, $amount, null);
}
$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 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 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.