CSS Hue rotate workaround

by AntowaKartowa

HTML

<!-- Hue rotate approaches test 
If you want to transition color through full Hue circle
starting from 0 degree and finish at 360 degree you probably
wouldn't get any transitions. Because hue(0deg 100% 65%) represents
same RGB as hue(360deg 100% 65%).

If you change 360 degree to 350 degree you will see transition. But
this transition woldn't satisfy expectation, because the RGB color 
interpolation would go the shorter path. It means it wouldn't go 
through all Hue circle (colors). 

And even if you change final Hue value to 180 degree it also wouldn't
go through half Hue circle colors. Rather it converts initial and final
color to RGB and then interpolate each numeric value of RGB to
transition. Besides it doesn't give you transition through half Hue 
circle colors it also lead to grey color in the middle of transition.

To avoid grey color you have to add a few intermediate colors which is
easier to acheive by animations (keyframes). But hardcoding colors not
always suitable and offten it is better to have some flexible keyframe
that can be applied to any initial (dynamic) value.

In this code sample I am exploring a few ways to make flexible Hue rotate
animation using CSS variables. Each button represents different approach:

1. Keyframes calculate --hue CSS variable (fully declared) based on other 
CSS variables (--hue-init and --hue-increment). Works fine in Chrome, but
staggered in Firefox.

2. Keyframes change --hue-change CSS variable (fully declared) from 0 to
180 that is used to calculate background-color property. Keyframes has
start and end value that is added to initial (any dynamic) value. Works
smoothely. But rotation degree value is fixed in particular animation.

3. Keyframes change numeric --step CSS variable (fully declared) from 0
to 10. Then we use it to calculate background-color property. Because step
is just abstract range, now we can multiply it by any dynamic value.
Multiplying 18 as a result we...

CSS

html {
  display: grid;
  place-content: center;
  height: 100vh;
  background: hsl(210deg 15% 6%);
}

.flex-column {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

button {
  border: transparent;
  padding: 1rem 3rem;
  border-radius: 8px;
}


@property --hue-init {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
@property --hue-increment {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
@property --hue-change {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
@property --hue {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
@property --step {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

/* v1 - Jumping in Firefox */
@keyframes rotateHue {
  0% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 0);
  }
  10% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 1);
  }
  20% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 2);
  }
  30% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 3);
  }
  40% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 4);
  }
  50% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 5);
  }
  60% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 6);
  }
  70% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 7);
  }
  80% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 8);
  }
  90% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 9);
  }
  100% {
    --hue: calc(var(--hue-init) + var(--hue-increment) * 10);
  }
}

/* v2.2 - Smooth */
@keyframes rotateHue180 {
  from {
    --hue-change: 0;
  }
  to {
    --hue-change: 180;
  }
}

/* v3 - Smooth */
@keyframes incrementStep {
  from {
    --step: 0;
  }
  to {
    --step: 10;
  }
}

/* v5 - Smooth */
@keyframes rotateHue180Filter {
  to {
    filter: hue-rotate(180deg)
  }
}

.button1 {
  --hue-increment:...

JavaScript

document.querySelectorAll('.button').forEach(button => {
  button.addEventListener('click', hangJSThread)
});

function hangJSThread() {
  const threashold = performance.now() + 5e3;
  var counter = 0;

  console.log('start');
  while (performance.now() < threashold) ++counter;
  console.log('finish');
}