24

In our design system at Stack Overflow, we use Less to compile CSS color values.

We have global Less variables like @orange-500 that are frequently modified for hover states, building border styling, background colors, etc.

In Less, this is written as darken(@orange-500, 5%). I'm trying to achieve something similar using native CSS variables. Switching to CSS variables will allow us to ship features that rely on theming (Stack Exchange Network, Dark Mode, etc.) much faster, with way fewer lines of CSS, while enabling swapping variables on media query (high contrast, dark mode, etc).

This example of overriding our color’s lightness value in hsl works when the variables are scoped to a CSS class:

.card {
  --orange: hsl(255, 72%, var(--lightness, 68%));
  background: var(--orange);
}
.card:hover {
  --lightness: 45%;
}
<div class="card">
  Hello world
</div>

However, we need to specify our color variables globally in a single, swappable place to support global theming, but this doesn't work as expected:

:root {
  --orange: hsl(255, 72%, var(--lightness, 68%));
}
.card {
  background: var(--orange);
}
.card:hover {
  --lightness: 45%;
}
<div class="card">
  Hello world
</div>

I've tried switching from :root to html or body without any luck. Any workarounds to this?

Aaron Shekey
  • 871
  • 1
  • 11
  • 22
  • 1
    It seems strange at first but when you really get down to it does it make sense for orange to change across the page when one thing gets hovered? – kpie Oct 02 '19 at 18:43
  • 1
    The intent is to take that global orange and modify its lightness by `x` amount only in the context of `.card:hover`. – Aaron Shekey Oct 02 '19 at 19:09
  • 1
    Yeah I get that, I'm just saying that once you realize that the `--lightness` defined in `:root` is a global variable and the `--lightness` defined in `.card:hover` is a local variable the behavior is as expected because `--orange' references the global `--lightness` not the local `--lightness` – kpie Oct 02 '19 at 19:23
  • 2
    short answer: it's impossible. if var() is used inside :root then it's over, it's already evaluated for all the DOM – Temani Afif Oct 02 '19 at 19:24
  • You can modify the global --lightness variable with JS `onmouseover` and `onmouseout`. Note that this will effect all references to orange and not just those in card. – kpie Oct 02 '19 at 19:30
  • 15
    This question [is being discussed on Meta](https://meta.stackoverflow.com/questions/390046/arent-we-allowed-to-close-question-asked-by-a-staff-member). – halfer Oct 02 '19 at 19:55

3 Answers3

22

This is a scoping issue. The way you're doing it, you're inheriting --orange from the :root, and --orange in the :root has a lightness of 68%.

In order to change it, you'll want to re-scope the --orange variable to an element that will look up the new --lightness value. There's a few ways to pull this off:

Option 1: duplicate the --orange variable on the element:

:root {
  --lightness: 68%;
  --orange: hsl(255, 72%, var(--lightness));
}
.card {
  background: var(--orange);
  --orange: hsl(255, 72%, var(--lightness));
}
.card:hover {

  --lightness: 45%;
}
<div class="card">
  Hello world
</div>

Obviously this kinda stinks, because you're going to have to duplicate that --orange variable.

Option 2: You could abstract the other parameters of --orange so that it's not as duplicative. I'd be a fan of this approach despite the fact that it's more text:

:root {
  --lightness: 68%;
  --orangeHue: 255;
  --orangeSat: 72%;
  --orange: hsl(var(--orangeHue), var(--orangeSat), var(--lightness));
}
.card {
  background: var(--orange);
  --orange: hsl(var(--orangeHue), var(--orangeSat), var(--lightness));
}
.card:hover {

  --lightness: 45%;
}
<div class="card">
  Hello world
</div>

What you could also do is scope this specifically to a .darkMode class that might be applied to the HTML element or the body. This could also make sense because it's clear what the intent is from the code:

Option 3

:root {
  --lightness: 68%;
  --orangeHue: 255;
  --orangeSat: 72%;
  --orange: hsl(var(--orangeHue), var(--orangeSat), var(--lightness));
}

.card {
  background: var(--orange);

}
.card:hover {
  --lightness: 45%;
}
.darkMode .card {
  --orange: hsl(var(--orangeHue), var(--orangeSat), var(--lightness));
}
  <div class="darkMode">
    <div class="card">
      Hello world
    </div>
  </div>

Regardless of how you go, the issue is that the --orange variable is inheriting from its original scope where --lightness is set. Think of it as "inheriting a computed value".

In order to get --orange to get the new lightness, you need a new --orange somewhere.

Option 4

I'm not sure what your theme pattern is, but I can explain how I created a dark mode on my own blog . If you look at the CSS What you'll see is that I've created two complete themes that follow the same naming convention:

--themeLightTextColor: rgb(55, 55, 55);
--themeLightBGColor: rgb(255, 255, 255);
--themeLightAccentColor: rgb(248, 248, 248);
--themeLightTrimColor: rgb(238, 238, 238);
--themeDarkTextColor: rgb(220, 220, 220);
--themeDarkBGColor: rgb(23, 23, 23);
--themeDarkAccentColor: rgb(55, 55, 55);
--themeDarkTrimColor: rgb(40, 40, 40);

What I then do is create a third set of variables whose job it is to be the "active" managers:

--themeActiveLinkColor: var(--linkColor);
--themeActiveLinkColorHover: var(--linkColorHover);
--themeActiveTextColor: var(--themeLightTextColor);
--themeActiveEditorialTextColor: var(--themeLightPltNLow);
--themeActiveBGColor: var(--themeLightBGColor);
--themeActiveAccentColor: var(--themeLightAccentColor);
--themeActiveTrimColor: var(--themeLightTrimColor);

Then, I scope the active theme settings under a single class:

.theme--dark {
   --themeActiveTextColor: var(--themeDarkTextColor);
   --themeActiveEditorialTextColor: var(--themeDarkPltNLow);
   --themeActiveBGColor: var(--themeDarkBGColor);
   --themeActiveAccentColor: var(--themeDarkAccentColor);
   --themeActiveTrimColor: var(--themeDarkTrimColor);
}

It seems like maybe your intent is to not have to explicitly declare a theme, but rather tweak some "root variables" to adjust it. But I would suggest that maybe you have a pattern in place where a single class can change an active theme. The advantage to this pattern is that you would be able to also adjust any "root variables" on the class name.

paceaux
  • 1,762
  • 1
  • 16
  • 24
6

I would be interested to learn if there is anything more ideal than this solution but as a possible workaround, you can break up your CSS variables a bit further and build the values inside the element style definitions like so:

:root {
  --orangeColor: 37,72%;
  --redColor: 1,72%;
  --blueColor: 215,72%;
  --greenColor: 126,72%;
  
  --LumDefault: 68%;
  --LumDark: 45%;
  --LumLight: 80%;
}
.card {
  background: hsl(var(--orangeColor), var(--LumDefault));
}
.card:hover {
  background: hsl(var(--orangeColor), var(--LumDark));
}
.card:active {
  background: hsl(var(--redColor), var(--LumDark));
  color: hsl(var(--greenColor), var(--LumLight));
}
<div class="card">
  Hello world
</div>

I do realize that this does not override as you wanted to accomplish but from the business case you stated, it will give you a way to manage elements at a global level...just a bit more work in defining your CSS on the front end.

TylerH
  • 20,799
  • 66
  • 75
  • 101
Travis Acton
  • 4,292
  • 2
  • 18
  • 30
1

The simple solution is to place the CSS variables into a separate CSS file and then swap it out as needed. For example, a media query to support dark mode could do the swap or you could use JavaScript, a pre-baked theme, etc.

What's nice about this is swapping the CSS file with your variable definitions changes the CSS rendering in real-time.

Assume you're using media queries for light/dark mode. If the browser understands and requests "dark mode" then only the first file is loaded. However, if the browser doesn't understand those media queries your "default" is the light.css since both CSS files are loaded but subsequent rules override previous rules.

<link rel="stylesheet" href="/dark.css" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" href="/light.css" media="(prefers-color-scheme: no-preference), (prefers-color-scheme: light)">
<!-- The main stylesheet -->
<link rel="stylesheet" href="/style.css">

Inside those stylesheets, you want to use the :root pseudo-class as it's basically the same as HTML, but has higher specificity in most browsers.

light.css

:root {
  --text-color: #333;
  --background-color: #fff;
}

dark.css

:root {
  --text-color: #dadada;
  --background-color: #333;
}

Also, note that simplifying the variables and building the full rule inside the element is a good idea as Travis mentions in that answer.

style.css (main styling document)

body {
  color: var(--text-color);
  background-color: var(--background-color);
}

As a side note, you may want to read up on the CSS color-scheme property to get a bit better support with native browser elements.

TylerH
  • 20,799
  • 66
  • 75
  • 101
Bryce Howitson
  • 7,339
  • 18
  • 40