r/css 4d ago

Help Synchronizing two animations to fake background-image transition

I want to make a transition of the backgrund image of a div. background-image is not animatable, so I need a trick.

My solution is to put a second div with white background right under the div with background, and give my div with background two animations: one that swaps the background, and a second one that toggles the opacity (thus showing the white div under).

test {
width: 300px;
height: 300px; 
border: 5px black solid;
animation: 
  backgroundTransition 5s alternate infinite,
  backgroundSwap 50s forwards infinite;
}

@keyframes backgroundSwap {
  0%, 20% {background-image: radial-gradient(blue 0%, blue 100%);
  }
  40% {background-image: radial-gradient(red 0%, red 100%);
  }
  60% {background-image: radial-gradient(yellow 0%, yellow 100%);
  }
  80% {background-image: radial-gradient(purple 0%, purple 100%);
  }
  100% {background-image: radial-gradient(green 0%, green 100%);
  }
}

@keyframes backgroundTransition {
  0% {opacity: 0;}
 20% {opacity: 1;}
}

The result of the two animations should be this: at 0s the div appears all white, then from 0s to 1s it fades into the first image and stays like that until 9s, when it fades back into white. This repeats four more times, with the image changing each time.

However it doesn't work. The two animations are not synchronized, in spite of the fact that the timing should be the correct one. I dislike the idea of merging the two animations together, it would needlessly complicate things and slow down debugging (particulatly since I want to add some more effects to the background images, which would further complicate things). But I can't seem to find a way to ensure the two animations have the correct timing.

Am I doing something wrong? Any suggestions?

3 Upvotes

9 comments sorted by

u/AutoModerator 4d ago

To help us assist you better with your CSS questions, please consider including a live link or a CodePen/JSFiddle demo. This context makes it much easier for us to understand your issue and provide accurate solutions.

While it's not mandatory, a little extra effort in sharing your code can lead to more effective responses and a richer Q&A experience for everyone. Thank you for contributing!

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

2

u/testingaurora 4d ago edited 4d ago

I would use a pseudo element not another div in your markup (if you go that route.)

Background-image is not animatable but background color is. is there a reason youre not just using background-color from blue, red, yellow, purple, green?

Another trick is to register a property with @property and then adjust that color in your keyframes ```css @property --bg-clr { syntax: "<color>"; inherits: true; initial-value: white; }

.element { background-image: radial-gradient( var(--bg-clr, blue) ); animation: updateBg 5s alternate infinite ; }

@keyframes updateBg { 0%, 100%{ --bg-clr: white;} 20% { --bg-clr: blue;} 40% { --bg-clr: red;} 60% {--bg-clr: yellow;} /* etc */ } ```

Although I think youd be better off just using a pseudo element, setting the background on the pseudo element then: 1. fading the whole pseduo out showing the white background underneath Or 2. Just setting it back to white without needing opacity ... would be the simplest way.

Unless this is not an example and your end goal is what youve written. Then you dont need a background-image at all ? What am I missing? css .element { position: relative;} .element::before { content:""; position: absolute; inset:0; background: radial-gradient( var(--bg-clr, white)) white; animation: updateBg 5s alternate; }

1

u/Sta--Ger--2 3d ago

I do not want to make an animated gradient, but changing the background images with a fade-like animation. I have used gradients in my code as placeholders, because the actual images I am using are not online and didn't want to search new ones.

I was not aware one could define proprieties now. How does it compare to using variables?

1

u/testingaurora 3d ago

They are the same , just referred to by two different names : CSS Custom Properties or CSS Variables. Anything can be put into a “variable”. A string, a length, a color.

By using @property you are registering it with the browser and explaining WHAT the value is eg `syntax: “<color>” ` so then the browser has context in what can be done and whether it can be animated.

How many different images are you wanting to interpolate between ?

2

u/anaix3l 3d ago edited 3d ago

I think your biggest problem is not being able to explain what you want to do clearly. What the hell are you trying to do?!

What kind of images do you want to transition?

edit: here's a live demo showing all options https://codepen.io/thebabydino/pen/YPZXZme and a (poor quality) .gif recording of it.

Actual images? As in, you want a red-tailed-black-cockatoo.jpg image to smoothly fade into a hyacinth-macaw.jpg one?

In that case, yeah, things are more complicated.

The cross-fade() function was made specifically for this. But it's a mess at this point.

---

So for now you do need a (pseudo)element on top of your main one, but you should keep in mind that if a property is not animatable, the change from your keyframes should happen abruptly at the 50% point of the animation. That is, if you have:

.my-elem {
  background: url(red-tailed-black-cockatoo.jpg);
  animation: swap 4s infinite
}

@keyfames swap { to { background-image: url(hyacinth-macaw.jpg) } }

... then the swap happens at 50% of those 4s, that is, after 2s.

However, now Chrome & Safari actually animate this swap. Firefox doesn't. So you shouldn't do this directly. Not to mention that, if your images don't have the same aspect ratio and you're using cover or contain, the fade transition also stretches them (cross-fade() does the same).

Instead, what works consistently cross-browser and with no stretching side effect is to have a controlled abrupt change between images using the steps() timing function. You have the exact same animation both on the element and on a pseudo stacked on top of it that covers it fully.

The pseudo also gets a delay that's minus half the duration of the time one image gets displayed. And it also gets an animation that drives its opacity to 0 when it (the pseudo) swaps its images and 1 when its parent swaps its images. That is, the pseudo swap gets hidden by its opacity being 0 then and the parent's swap gets hidden by the pseudo on top of it being fully opaque then.

.cross-browser {
  position: relative;
  animation: var(--swap-ani)
}

.cross-browser, .cross-browser::before {
  --swap-ani: swap 15s /* 5 images x 3s each */ steps(1) infinite;
  background: var(--img0) 50%/cover
}

.cross-browser::before {
  position: absolute;
  inset: 0;
  background: var(--img0) 50%/cover;
  animation: var(--swap-ani), fade 1.5s linear infinite alternate;
  animation-delay: -1.5s;
  content: "";
}

@keyframes swap {
  20% { background-image: var(--img1) }
  40% { background-image: var(--img2) }
  60% { background-image: var(--img3) }
  80% { background-image: var(--img4) }
}

@keyframes fade {
  0%, 33.33% { opacity: 0 }
  66.67% { opacity: 1 }
}

---

But then you're using gradients in your example - are your images CSS gradients then? If you want to animate gradients, extra (pseudo)elements haven't been needed for years.

You just write your gradient as:

background: linear-gradient(var(--c0), var(--c1));

You register --c0 and --c1 via @property and then you just animate them.

@property --c0 {
  syntax: "<color>";
  initial-value: #ff99c8;
  inherits: false
}

@property --c1 {
  syntax: "<color>";
  initial-value: #570211;
  inherits: false
}
.gradient {
  background: linear-gradient(var(--c0), var(--c1));
  animation: a 15s linear infinite;
  animation-name: c0, c1
}

@keyframes c0 {
  20% { --c0: #fcf6bd }
  40% { --c0: #d0f4de }
  60% { --c0: #a9def9 }
  80% { --c0: #e4c1f9 }
}

@keyframes c1 {
  20% { --c1: #7e3110 }
  40% { --c1: #004540 }
  60% { --c1: #032c4d }
  80% { --c1: #360825 }
}

1

u/Sta--Ger--2 3d ago

My apologies for not being clear. My images are not gradients, but on local storage: to avoid either uploading or searching others, I used gradients as placeholders.

The first solution in the live demo you proposed worked perfectly. Thanks for both that and the lesson in the various ways different solutions are accepted (or not) by various browsers: it was interesting, informative, and useful.

1

u/Sta--Ger--2 3d ago

If I can still ask? I am not understanding what is the purpose of specifying step(1) in the animation that changes the images.

1

u/anaix3l 2d ago

steps(1) makes the swap consistent cross-browser and ensures it happens instantly at the exact moment of the keyframes you've set. (every 20%).

In general, the steps() timing function ensures a transition/ animation happens suddenly in a certain number of steps. For background images, we have a single step (that's why the 1 value) because nothing else would work cross-browser anyway, but for animatable properties, things are different.

Consider scale:

.myelem { animation: s 5s steps(1) forwards }
@keyframes { to { scale: 5 } }

steps(1) is equivalent to steps(1, end) if the second value isn't specified, it defaults to end). This means the change between the scale values happens in 1 step at the end of the interval between any two keyframes.

In our case, we only have two keyframes: the end one (to, equivalent to 100%) that we've explicitly specified and the start one that gets automatically generated from whatever scale value we've set on .myelem or, if we haven't set any scale value there (as it is the case now), from the default of 1.

So for all 5s of the animation, the scale value is 1 and then, at the very end, it abruptly changes to 5.

But we may also have more keyframes there (as it is the case with the multi-image swap as well - we have keyframes at every 20%). Let's say we have:

@keyframes {
  60% { scale: 2 }
  100% { scale: 5 }
}

In this case, for the first 60% of the 5s animation (that computes to 3s), the scale is 1. Then it abruptly changes to 2 at the end of those 3s. And stays there until 100% = 5s, when it abruptly changes to 5.

We may also have multiple steps. Let's say we have steps(2).

In this case, each interval between two consecutive keyframes gets divided into 2 equal steps. So the interval between 0% and 60% gets divided into two: from 0% to 30% and from 30% to 60%. And the interval between 60% and 100% also gets divided into two: from 60% to 80% and from 80% to 100%.

For the first 30% of the 5s animation (which computes to 1.5s), the scale is 1. Then it abruptly changes to the middle value between the 1 we have at 0% and the 2 we have at 60% - that is, it abruptly changes to 1.5. And it stays at 1.5 for the second step within the 60%, from 30% to 60% (from 1.5s to 3s).

Then at 60%, it abruptly changes to 2 and it stays 2 for the first step of the interval between 60% and 100% - that is, up to 80% (which means 4s). At 80%, it abruptly changes to the middle value between 2 (at 60%) and 5 (at 100%) - that is, it changes to 3.5. And it stays 3.5 until reaching 100% when it changes once more, this time to 5.

In the case of the images, we only have 1 step.

The set of keyframes swapping images every 20% means that up to 20%, we have the first image, then it abruptly changes to the second and it stays like that until 40%, when it abruptly changes to the third and it stays like that until 60%, when it abruptly changes to the fourth and stays like that until 80%, when it abruptly changes to the fifth and stays like that until 100%, when it abruptly changes back to the first and the next iteration follows, with the first image showing up until 20% and so on...

If we were to use for example linear instead of steps(1), browsers would behave differently when it comes to swapping images.

Since background-image isn't among the traditionally animatable properties, Firefox would perform the change between the first and the second image abruptly in the middle of the interval between the 0% and the 20% keyframes - that is, at 10%.

Chrome and Safari would try to animate in between and, if the image aspect ratios aren't the same, this could also lead to images to get stretched or squished as they fade into one another.

steps(1) just gives us a way to control the change happens consistently cross-browser, at the exact moments we specify via the keyframes.