I recently made an interesting observation in my CSS that took me a little while to understand exactly what was happening.
For example, consider the following code snippet, in which the parent and child elements receive exactly the same background-color
at a given scroll value. Both elements also have the same transition defined for the background
property.
As you scroll, notice how the background color of the child elements clearly differs from the background color of the parent element during the transition.
function toggleClassOnScroll() {
const scrollPosition = window.scrollY;
const threshold = 40;
if (scrollPosition > threshold) {
document.body.classList.add("js-scrolled");
} else {
document.body.classList.remove("js-scrolled");
}
}
toggleClassOnScroll();
window.addEventListener("scroll", toggleClassOnScroll);
:root {
--scroll-transition: background 1s ease;
--scroll-background: darkgrey;
}
nav {
background-color: transparent;
transition: var(--scroll-transition);
}
.js-scrolled nav {
background-color: var(--scroll-background);
}
nav a {
background-color: transparent;
transition: var(--scroll-transition);
}
.js-scrolled nav a {
background-color: var(--scroll-background);
}
/* setup (visual) */
body {
min-height: 200vh;
}
nav {
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
border-bottom: 2px dashed var(--scroll-background);
width: 100%;
}
nav ul {
display: flex;
gap: 1rem;
margin: 0;
padding: 0;
list-style-type: none;
}
nav a {
position: relative;
display: block;
padding: 0.75em;
font-size: 1.5rem;
text-decoration: none;
color: black;
}
nav a:before {
content: "";
position: absolute;
inset: 0;
background-color: inherit;
mix-blend-mode: multiply;
opacity: 0;
}
nav a:hover::before,
nav a:focus::before {
opacity: 1;
}
<nav>
<ul>
<li><a href="#">Colors</a></li>
<li><a href="#">Transparency</a></li>
<li><a href="#">Transitions</a></li>
<li><a href="#">About</a></li>
</ul>
</nav>
Please note that in my use case, it is necessary that the child elements also receive the background color because of the
mix-blend-mode
hover effect on the nav item links.
I think I have an explanation for this (rendering) behavior of the different background colors during the transition:
Since both background colors have the initial value set to transparent
, an alpha channel is introduced, which then technically animates from 0 to 1, resulting in the full color defined on scroll.
So it should be the same as defining the background-color
in, e.g., rgb()
notation with an initial alpha value of 0 and setting this color with an alpha value of 1 on scroll.
rgb(169 169 169 / 0) -> rgb(169 169 169 / 1)
Because of the overlapping background colors with transparency, we get different colors during the transition.
This can be illustrated with a simple Venn diagram:
Attribution: Amousey, CC0, via Wikimedia Commons
Depending on the defined background-color
, transition-duration
, and transition-timing-function
, I personally find this behavior visually quite disturbing.
I was wondering if there is a way (or a tool) to prevent the visual difference during the transition.
Perhaps there is a mathematical function that can be used to calculate this based on the target color, the animation and the overlapping layer?
Or am I thinking too complicated? Is this situation actually common, and is there a ("simpler") solution?
You might set background-color
of link element on :hover
only:
function toggleClassOnScroll() {
const scrollPosition = window.scrollY;
const threshold = 40;
if (scrollPosition > threshold) {
document.body.classList.add("js-scrolled");
} else {
document.body.classList.remove("js-scrolled");
}
}
toggleClassOnScroll();
window.addEventListener("scroll", toggleClassOnScroll);
:root {
--scroll-transition: background 1s ease;
--scroll-background: darkgrey;
}
nav {
background-color: transparent;
transition: var(--scroll-transition);
}
.js-scrolled nav {
background-color: var(--scroll-background);
}
.js-scrolled nav a:hover,
.js-scrolled nav a:focus {
background-color: var(--scroll-background);
}
/* setup (visual) */
body {
min-height: 200vh;
}
nav {
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
border-bottom: 2px dashed var(--scroll-background);
width: 100%;
}
nav ul {
display: flex;
gap: 1rem;
margin: 0;
padding: 0;
list-style-type: none;
}
nav a {
position: relative;
display: block;
padding: 0.75em;
font-size: 1.5rem;
text-decoration: none;
color: black;
}
nav a:before {
content: "";
position: absolute;
inset: 0;
background-color: inherit;
mix-blend-mode: multiply;
opacity: 0;
}
nav a:hover::before,
nav a:focus::before {
opacity: 1;
}
<nav>
<ul>
<li><a href="#">Colors</a></li>
<li><a href="#">Transparency</a></li>
<li><a href="#">Transitions</a></li>
<li><a href="#">About</a></li>
</ul>
</nav>
Also I think your code is a bit excessive, but probably there's a reason for that.