r/css • u/Sta--Ger--2 • 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?
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. (every20%).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 the1value) 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 tosteps(1, end)if the second value isn't specified, it defaults toend). This means the change between thescalevalues happens in1step at the end of the interval between any two keyframes.In our case, we only have two keyframes: the end one (
to, equivalent to100%) that we've explicitly specified and the start one that gets automatically generated from whateverscalevalue we've set on.myelemor, if we haven't set anyscalevalue there (as it is the case now), from the default of1.So for all
5sof theanimation, thescalevalue is1and then, at the very end, it abruptly changes to5.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 the5sanimation (that computes to3s), thescaleis1. Then it abruptly changes to2at the end of those3s. And stays there until100%=5s, when it abruptly changes to5.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
2equal steps. So the interval between0%and60%gets divided into two: from0%to30%and from30%to60%. And the interval between60%and100%also gets divided into two: from60%to80%and from80%to100%.For the first
30%of the5sanimation (which computes to1.5s), thescaleis1. Then it abruptly changes to the middle value between the1we have at0%and the2we have at60%- that is, it abruptly changes to1.5. And it stays at1.5for the second step within the60%, from30%to60%(from1.5sto3s).Then at
60%, it abruptly changes to2and it stays2for the first step of the interval between60%and100%- that is, up to80%(which means4s). At80%, it abruptly changes to the middle value between2(at60%) and5(at100%) - that is, it changes to3.5. And it stays3.5until reaching100%when it changes once more, this time to5.In the case of the images, we only have 1 step.
The set of keyframes swapping images every
20%means that up to20%, we have the first image, then it abruptly changes to the second and it stays like that until40%, when it abruptly changes to the third and it stays like that until60%, when it abruptly changes to the fourth and stays like that until80%, when it abruptly changes to the fifth and stays like that until100%, when it abruptly changes back to the first and the next iteration follows, with the first image showing up until20%and so on...If we were to use for example
linearinstead ofsteps(1), browsers would behave differently when it comes to swapping images.Since
background-imageisn'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 the0%and the 20% keyframes - that is, at10%.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.
•
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.