1

A toggle switch button in the top right toggles from light to dark mode and vice versa. The background is an image and zoomed in.

Currently, it moves from

  • left to right when switched to dark mode
  • right to left when switched to light mode

But I want it to move in one direction itself. Like, suppose it's in light mode, and when I switch it to dark mode the background image shifts to the right (left > right) like it's supposed to, and then when I switch it to light mode I want the background image to go from left to right again (left > right).

Kinda like a continuous loop but with a toggle button.

Is there any way I can achieve that?

I guess it might work with keyframes and animations not sure. But is there a way to do it in JS without the need for keyframes?

Don't mind the code below cause I had to clip it from my react project so some of the CSS property might not seem needed.

let Dark = false
const DarkOrLight = () => {
  const container = document.getElementsByClassName('Login-container')[0]
  if (!Dark) {
    container.style.backgroundPosition = 'right'
  } else {
    container.style.backgroundPosition = 'left'
  }
  Dark = !Dark
}
.toggle-container {
  position: fixed;
  top: 5px;
  right: 5%;
}

.toggle {
  appearance: none;
  width: 50px;
  height: 30px;
  background-image: linear-gradient(120deg, #383030 0%, #22282e 100%);
  cursor: pointer;
  border-radius: 20px;
}

.toggle:focus {
  outline: 0;
}

.toggle::after {
  content: '';
  background-image: linear-gradient(120deg, rgb(182, 244, 146), rgb(51, 139, 147));
  position: absolute;
  top: 5px;
  left: 5px;
  width: 25px;
  height: 25px;
  border-radius: 50%;
  transition: all 1s ease;
}

.toggle:checked {
  background-color: #b6f492;
  background-image: linear-gradient(120deg, #b6f492, rgb(51, 139, 147));
}

.toggle:checked::after {
  transform: translateX(23px);
  background-image: linear-gradient(120deg, #383030 0%, #22282e 100%);
}

.Login-container {
  position: relative;
  background-image: linear-gradient(120deg, #b6f492, #338b93, #383030, #22282e);
  background-size: 400%;
  background-position: left;
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: end;
  transition: all 1s ease;
  font-family: Merriweather;
}
<div class="Login-container">
  <div class="toggle-container">
    <input class="toggle" type="checkbox" onClick=DarkOrLight()></input>
  </div>
</div>
Christian
  • 4,902
  • 4
  • 24
  • 42
  • 1
    Your distribution of colors currently looks like: `0....100|0....100|` instead you need to provide a way to ***come back home***: `0...100...0|0...100...0|`. The rest is pure math. In JS increment a variable `bgPos` by `1` on each click, and move it `background-position` left by `n% * bgPos`, where `n` depends on the number of your color-stops in relation to the lightModeColors (transitionColors) darkModeColors (transitionColors) . – Roko C. Buljan Mar 27 '22 at 22:31
  • And since you did not provided the colors gradients needed to ***come back home*** (to light mode) - it's pretty hard to answer. Also, as you're currently doing, your colors-stops are **off by one**! — since you use a background-size of 400%. – Roko C. Buljan Mar 27 '22 at 22:34
  • PS: the only culprit in your task is that, you need to take into consideration that: by moving a background-image in % (instead of px) 100% does not means that the background image will be back at its starting position. `100%` is exactly the same as setting it to `right`. Here's a very detailed explanation: https://stackoverflow.com/questions/51731106/using-percentage-values-with-background-position-on-a-linear-gradient – Roko C. Buljan Mar 28 '22 at 00:34

1 Answers1

1

Fix your color-stops:

  • you have a background-size of 400%
  • but you use only 4 color-stops (instead of 5).

The problem: by simply animating left to right this happens with your current code:

|left                          right|       bg-position left-to-right
|#1         |#2         |#3         |#4     4 colors-stops 
|0%      |100%    |200%    |300%    |400%   bg-size
| light  |     (anim)      |  dark  |       mode
|░░░░░░░░|▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|▓▓▓▓▓▓▓▓|          

Do you know what exact color is at 100%? No.
Do you know what exact color is at 300%? No.
See the inconsistency? The color stops are off by one.

Some possible solutions to the above:

  • add one more color-stop -or
  • reduce the width to 300% -or
  • use CSS linear-gradient() stops at the desired % position

Suggestion

Animate a child element that has that desired background:

  • Width of 400%
  • 5 color-stops, where the last one matches the first!
|0%               |200%             |400%   animate left by -%
|#1      |#2      |#3      |#4      |#1     5 colors-stops 
|0%      |100%    |200%    |300%    |400%   width
| light  | (anim) |  dark  | (anim) |       mode 
|░░░░░░░░|▒▒▒▒▒▒▒▒|▓▓▓▓▓▓▓▓|▒▒▒▒▒▒▒▒|░░...

as you can see, on 400% we're back on color #1 - ideal for the loop logic! And by "animate left by -%" I mean animate the GPU accelerated transform: translateX() property value.

But wait! What if I want to animate the background-position instead of an Element child?
Isn't background-position: 100% 0; exactly the same as background-position: right 0?
Sadly, yes. 100% will not set it at its initial state (0%). The above table would make sense if we animated a child element to steps of -200% left position.
For an in-depth explanation of this (background-position) issue read: Using percentage values with background-position on a linear-gradient.

Solution

You could either:

  1. Use a child element (with the background) set to 4× the width of your parent element (width: 400%;). Then in JS, move it left by -200%, and on transitionend Event you could reset its position to 0 (if goin back to Light mode) using the Modulo (Reminder) operator %.

  2. Otherwise, the math for moving the background is actually in thirds:

Formula: 100 / 3 * totModes * counter
In the table below being bgX = 100 / 3 * 2 * (1) = 66.66666666666667 etc...

|0%               |66.66%           |133.33% bg-position by %
|#1      |#2      |#3      |#4      |#1      5 colors-stops 
|0%      |100%    |200%    |300%    |400%    bg-size
| light  | (anim) |  dark  | (anim) |        mode 
|░░░░░░░░|▒▒▒▒▒▒▒▒|▓▓▓▓▓▓▓▓|▒▒▒▒▒▒▒▒|░░...

Example:

// DOM utility functions:

const find = (selector, parent) => (parent || document).querySelector(selector);



// Task:

const elContainer = find("#login");
let c = 0; // position counter

const toggleDarkMode = () => {
  c += 1;
  const bgX = 100 / 3 * 2 * c;
  console.log(bgX)
  elContainer.style.backgroundPosition = `${bgX}%`;
};

find("#toggleMode").addEventListener("input", toggleDarkMode);
* {
  margin: 0;
}

.toggle-container {
  position: fixed;
  z-index: 1000;
  top: 20px;
  left: 20px;
}

.Login-container {
  background-image: linear-gradient(to right,
    #fff,
    red,
    #000,
    blue,
    #fff /*back to home*/
  );
  background-size: 400%;
  background-position: 0% 0;
  background-repeat: repeat-x;
  height: 100vh;
  transition: background 1s ease;
}
<label class="toggle-container">
  DARK MODE: <input id="toggleMode" class="toggle" type="checkbox">
</label>
<div id="login" class="Login-container"></div>

Diagonal gradients?

As you can see, the above will work only if the background-image's linear-gradient() is either set to 90deg, 270deg, to right or to left.

For Diagonal gradients any other degrees you could duplicate the values and move by half. But in that case the background-image should be positioned with negative offsets. And again, I think the animationend would finally come into play.

Not sure if the above helps, but hopefully it will clear some thoughts and help you find a viable solution.

Roko C. Buljan
  • 196,159
  • 39
  • 305
  • 313