52

I'm looking a way of modifying a CSS variable as you would in SCSS

Define a color like primary - and automatically I would get shades for focus and actives states. Basically, would like to change one variable in css variables and get 3 shades of the same color.

What Id like to achieve in CSS

$color-primary: #f00;

.button {
    background: $color-primary;

    &:hover,
    &:focus {
        background: darken($color-primary, 5%);
    }

    &:active {
        background: darken($color-primary, 10%);
    }
}

trying to achieve:

:root {
    --color-primary: #f00;
    --color-primary-darker: #f20000  //     var(--color-primary) * 5% darker
    --color-primary-darkest: #e50000 //     var(--color-primary) * 10% darker
}

.button {
    background: var(--color-primary);
}

.button:hover,
.button:focus {
    background: var(--color-primary-darker);
}

.button:active {
    background: var(--color-primary-darkest);
}
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
Daniel
  • 815
  • 1
  • 6
  • 13

4 Answers4

103

The new Specification introduces "relative color syntax" where you can do the following

:root {
    --color-primary: #f00; /* any format you want here */
    --color-primary-darker: hsl(from var(--color-primary) h s calc(l - 5%)); 
    --color-primary-darkest: hsl(from var(--color-primary) h s calc(l - 10%)); 
}

The idea is to convert the main color to hsl format and using calc() you adjust the lightness.

There is still no support for this to date so consider the below solution.

You can use color-mix() and mix the color with black (or white) to create different shades from the same color.

html {
  --color-primary: #8A9B0F; 
  --color-primary-darker:  color-mix(in srgb,var(--color-primary),#000 15%);
  --color-primary-darkest: color-mix(in srgb,var(--color-primary),#000 30%);
  
  background:
    linear-gradient(to right,var(--color-primary) 33%,var(--color-primary-darker) 0 66%,var(--color-primary-darkest) 0);
}

Related: https://css-tip.com/color-shades-color-mix/


Old Answer

You can consider hsl() colors and simply control the lightness:

:root {
    --color:0, 100%; /*the base color*/
    --l:50%; /*the initial lightness*/
    
    --color-primary: hsl(var(--color),var(--l));
    --color-primary-darker: hsl(var(--color),calc(var(--l) - 5%));
    --color-primary-darkest: hsl(var(--color),calc(var(--l) - 10%)); 
}

.button {
    background: var(--color-primary);
    display:inline-block;
    padding:10px 20px;
    color:#fff;
    cursor:pointer;
}

.button:hover,
.button:focus {
    background: var(--color-primary-darker);
}

.button:active {
    background: var(--color-primary-darkest);
}
<span class="button">some text</span>

As a side note, darken() is also doing the same thing:

Makes a color darker. Takes a color and a number between 0% and 100%, and returns a color with the lightness decreased by that amount.

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • How would one set another color for the initial --color variable? I'm not sure how `0, 100%` is equal to red. – Bribbons Mar 09 '20 at 06:42
  • 1
    @Bribbons use a converter: https://www.rapidtables.com/convert/color/rgb-to-hsl.html – Temani Afif Mar 09 '20 at 09:25
  • 1
    @TemaniAfif solution works. But instead of hard coding the lightness value, is there a way to retrieve the value from a color on the go. See, I have like a dozen colors and I would like get a darker version of all – Dibzmania May 17 '20 at 04:43
  • @Dibzmania can you share a use case and a code? based on the situation we can find a solution – Temani Afif May 17 '20 at 10:26
  • @TemaniAfif I will work on providing the code. But for the use case, our website has 4-5 different themes that the user can choose from. Owing to using Bootstrap, there are places in CSS where calls like darken and lighten are made. Using CSS variable, I am able to change the primary/accent/secondary colors etc. during runtime. However, SCSS methods like darken which for what they are can only take SCSS variables for now. – Dibzmania May 17 '20 at 15:37
  • I don't find the improvement moving away from Sass vars. This looks more complex. At the end we are bringing more complexity to css to "fix" something that was already fixed by Sass/Less – Dani Sep 29 '20 at 08:20
  • 4
    @Dani Sass and Less variables are compiled before runtime. CSS vars can be changed at runtime. This is something Sass and Less just can't do. This means you can, for example, change the value of a variable in response to a user action. Very helpful for developing light/dark modes. This would be far more complex, if not impossible, to do in Sass/Less. – Michael Brook Mar 23 '21 at 22:07
  • Nice solution, but what if the base color is created with `hsl`? – Shahriar Feb 05 '22 at 13:16
  • https://jsfiddle.net/Lmqrfs20/ It seems to not yet supported in **any** browsers. Tested in Firefox, Chrome, Edge but none of them work. – vee Mar 08 '22 at 09:19
  • @vee did you read my answer *fully* ? I explicitly said that there is no support for it ... – Temani Afif Mar 08 '22 at 17:51
  • @TemaniAfif Sorry, I look only browser keyword. :-P – vee Mar 08 '22 at 19:08
  • where does the `from var() h s ...` syntax come from? can't find anything about it and it doesn't work in my vue nuxt project – Branchverse Oct 19 '22 at 10:49
  • 1
    @MaximilianDolbaum read the answer again. (1) there is a link to the specification (2) I said in **bold** that there is no support for this syntax. – Temani Afif Oct 19 '22 at 10:57
8

How about this (pure sass/scss):

First, we need to split a color into hsla values and save each one in a separate custom property. Luckily sass has some functions to do the job.

@mixin define-color($title, $color) {
    --#{$title}-h: #{hue($color)};
    --#{$title}-l: #{lightness($color)};
    --#{$title}-s: #{saturation($color)};
    --#{$title}-a: #{alpha($color)};
}

Now we can put it back together, making some adjustments on the way.

@function color($title, $hue: 0deg, $lightness: 0%, $saturation: 0%, $alpha: 0) {
    @return hsla(
        calc(var(--#{$title}-h) + #{$hue}), 
        calc(var(--#{$title}-s) + #{$saturation}),
        calc(var(--#{$title}-l) + #{$lightness}),
        calc(var(--#{$title}-a) + #{$alpha}),
    );
}

Now we are ready to define some color variables...

:root {
    @include define-color("primary", #696969);
    @include define-color("secondary", blue);
}

override them (to dynamically switch between themes for example)...

:root.theme-light {
    @include define-color("primary", #424242);
    @include define-color("secondary", red);
}

use and adjust them!

.example-class {
    color: color("primary");
    background: color("secondary", $lightness: +20%, $alpha: -0.3);
    border: 1px solid color("primary", $hue: -30deg, $saturation: 5%);
}
Romalex
  • 1,582
  • 11
  • 13
1

If you are willing to take a different approach to your problem, using masks with the pseudo ":before" element would solve your problem. Although if you use this, i would advice you to put any content in the button inside a span or something, to give it a "z-index:1", so the content is not behind the mask.

:root {
    --color-primary: #f00;
}

.button {
    position:relative;
    background: var(--color-primary);

    &:before {
        content:'';
        position:absolute;
        width:100%;
        height:100%;
        top:0;
        left:0;
    }
}

.button:hover:before,
.button:focus:before {
    background:rgba(0,0,0,0.05) /* black mask with 5% opacity */
}

.button:active:before {
    background:rgba(0,0,0,0.1) /* black mask with 10% opacity */
}
1

Expanding on Temanis answer: I use a gradient - from black to a dynamic color to white - and expand the background 100 times. Now its only a question of positioning the background.

In the CSS

.dynamic-color {
    --lighten: 80%;
    --darken: 45%;
    --original-color: 50%;
    --color-intensity: var(--original-color);
    --color-variable: blue;
    background-image: linear-gradient(90deg,black, var(--color-variable),white);
    background-repeat: no-repeat;
    background-size: 10000% 100%;
    background-position-x: var(--color-intensity);
}

.dynamic-color:hover{
    --color-intensity: var(--lighten);
}

.dynamic-color.active{
    --color-intensity: var(--darken);
}

And in the HTML

<btn class="dynamic-color" style="--color-variable: green">Hover me</btn>