PostsNow
cover image for this post
4 June 2020

Unexpected CSS Variable Behavior in Chrome 83

I just revamped this site's homepage and providing a light/dark theme. A few days later, the dark mode theme didn't work as expected.

I've opened it using my phone and switched it to dark mode. The expected behavior is that the font color switched to white in dark mode, but it turns to black instead. I tried with other browsers (Firefox, Edge, and Opera) and it works fine. I also tried using Chrome in my wife's phone, and it also worked. So then I proceed to check the Chrome app version.

Bug in this website
Image 1: Unexpected text color when switching theme

During those few days, my apps was being auto updated. My Chrome version is 83.0.4103.61, while my wife's Chrome is 81.x.x.x (forgot the detailed version). So does the newer version of Chrome is buggy? Should I report this as a bug?

Self Investigation

Before jumping to the conclusion, we should do some investigation first. So here are my CSS that handles the dark mode theme.

:root,
:root[data-theme="light"] {
  --bg-color: #fcfcfc;
  --text-color: rgb(82, 82, 94);
  --link-color: #2c7a7b;
  --is-dark: 0;
}

:root[data-theme="dark"] {
  --bg-color: rgb(82, 82, 94);
  --text-color: #ffffff;
  --link-color: #faf089;
  --is-dark: 1;
}

html {
  background-color: var(--bg-color);
  color: var(--text-color);
}
Code 1: CSS that handles switching theme

Clicking the theme change handler toggles the data-theme property on html tag. From the value of the data-theme, I defined the CSS variables for each theme.

So, what causes the 'bug'?

The common practice of declaring CSS variable is on :root. A variable that is declared in the global scope can be accessed anywhere in the CSS. Other than the global scope, CSS also has a local scope, which means the value of the CSS variables follow the nearest ancestor where it was declared.

In HTML, :root represents the <html> element and is identical to the tag html. Let's have a look again at the CSS.

:root,
:root[data-theme="light"] {
  --bg-color: #fcfcfc;
  --text-color: rgb(82, 82, 94);
  --link-color: #2c7a7b;
  --is-dark: 0;
}

:root[data-theme="dark"] {
  --bg-color: rgb(82, 82, 94);
  --text-color: #ffffff;
  --link-color: #faf089;
  --is-dark: 1;
}

html {
  background-color: var(--bg-color);
  color: var(--text-color);
}
Code 2: Relook at CSS that handles switching theme

Here, the variables are defined in the :root selector (which is also html), and the variables are being used in html selector.

The findings of this unexpected bug are:

  • The font color changes to the default pure black (#000) like the ones when we didn't define any additional style.
  • Link color and background color changes correctly. Font color is the only one that is misbehaving.
  • It happened intermittently. Sometimes it happened in the light theme, sometimes it's on dark theme. At other times, the bug didn't happen.

Somehow in Chrome 83, maybe something related to CSS variable is changed. Declaring and accessing the variable at the same level produces an unexpected result. Maybe there are some race condition or something?

Link color are changed as expected, maybe because the link color is defined separately in .link selector, which is clearly inside the :root scope. But why does the background color that is also have the same conditions as font color works normally? No clue for now.

The Fix

How I fixed this to the expected behavior is quite simple actually. I moved where I would read and apply the value of the CSS variables value from html to body.

:root,
:root[data-theme="light"] {
  --bg-color: #fcfcfc;
  --text-color: rgb(82, 82, 94);
  --link-color: #2c7a7b;
  --is-dark: 0;
}

:root[data-theme="dark"] {
  --bg-color: rgb(82, 82, 94);
  --text-color: #ffffff;
  --link-color: #faf089;
  --is-dark: 1;
}

body {
  background-color: var(--bg-color);
  color: var(--text-color);
}
Code 3: Fixed CSS

Until I published this post, I haven't found the clear answer about this behavior. Is this the real expected behavior that the selector that reads the value must be the descendant where it was declared? Or did the previous version that allows the variable declaration and usage at the same level is the right one?

I'm not sure enough to decide which is right, therefore I'm not submitting a bug report yet, or I might be the one in the wrong here 🙇🏻‍♂️.

Should there be any follow up about this, I will update this post.

I'm writing this as my debugging journal. I hope it's helpful to some of you who encountered the same problem.