CSS gradients can't be animated... right?

Author Jesse Breneman Published on May 25, 2025

I was browsing my Codepen the other day and came across a pen I had made when @property was first available to test but not well supported. Now that @property is pretty well supported, animated CSS gradients are just... possible now. I threw together a few more ideas and then decided that a quick note of how to do this and some demos would make a great blog post.

How does this work?

It's as simple as registering any part of the gradient that you want to change, and then animating it with a transition or an animation.

css
@property --color-1 {
    syntax: "<color>";
    inherits: false;
    initial-value: #009688;
}

@property --color-2 {
    syntax: "<color>";
    inherits: false;
    initial-value: #00bcd4;
}

.gradient {
    background: linear-gradient(in oklab to right, var(--color-1), var(--color-2));
    animation: change-colors
}

@keyframes change-colors {
    100% {
        --color-1: #E91E63;
        --color-2: #9c27b0;
    }
}

For the purpose of the rest of the demos, I'm going to abstract the base styles into an .animated-gradient class and declare the custom properties ahead of time, and then each individual demo will get an additional class and animation.

css
@property --color-1 {
    syntax: "<color>";
    inherits: false;
    initial-value: #009688;
}

@property --color-2 {
    syntax: "<color>";
    inherits: false;
    initial-value: #00bcd4;
}

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

@property --color-4 {
    syntax: "<color>";
    inherits: false;
    initial-value: #9c27b0;
}

@property --color-5 {
    syntax: "<color>";
    inherits: false;
    initial-value: #9c27b0;
}

@property --color-6 {
    syntax: "<color>";
    inherits: false;
    initial-value: #9c27b0;
}

@property --angle {
    syntax: "<angle>";
    inherits: false;
    initial-value: 0turn;
}

@property --stop-1 {
    syntax: "<percentage>";
    inherits: false;
    initial-value: 50%;
}

.animated-gradient {
    --direction: to bottom right;
    --easing: ease;
    --length: 10s;
    width: 400px;
    height: 200px;
    background: linear-gradient(in oklab var(--direction), var(--color-1), var(--color-2));
    animation: var(--animation) var(--length) var(--easing) infinite;
}

Demos

Color to color

css
.one {
    --color-1: #009688;
    --color-2: #00bcd4;
    --animation: move;
}

@keyframes move {
    50% {
        --color-1: #E91E63;
        --color-2: #9c27b0;
    }
}

Side to side

css
.two {
    --color-1: #009688;
    --color-2: #00bcd4;
    --animation: side;
    --direction: to right;
    --easing: linear;
}

@keyframes side {
    0% {
        --color-1: #009688;
        --color-2: #00bcd4;
    }
    25% {
        --color-1: #00bcd4;
        --color-2: #E91E63;
    }
    50% {
        --color-1: #E91E63;
        --color-2: #9c27b0;
    }
    75% {
        --color-1: #9c27b0;
        --color-2: #009688;
    }
    100% {
        --color-1: #009688;
        --color-2: #00bcd4;
    }
}

Pulse

css
.three {
    --color-1: #009688;
    --color-2: #9c27b0;
    --animation: pulse;
    --length: 2s;
    --easing: ease-in-out;
    background: radial-gradient(var(--color-1), var(--color-2));
}

@keyframes pulse {
    50% {
        --color-2: #009688;
    }
}

Spin

css
.four {
    --animation: spin;
    --easing: linear;
    --length: 2s;
    background: linear-gradient(var(--angle), var(--color-1), var(--color-2) 50%, var(--color-3) 50%, var(--color-4));
}

@keyframes spin {
    0% {
        --angle: 0turn;
    }
    50% {
        --color-1: var(--color-2);
        --color-4: var(--color-3);
    }
    100% {
        --angle: 1turn;
    }
}

Moving stops

css
.five {
    --animation: stops;
    --length: 5s;
    background: linear-gradient(to right, var(--color-1), var(--color-2) var(--stop-1), var(--color-3) var(--stop-1), var(--color-4));
}

@keyframes stops {
    0% {
        --stop-1: 100%;
    }
    100% {
        --stop-1: 0%;
    }
}

Multicolor cloud

css
.six {
    --color-1: rgba(255, 0, 0, 0.8);
    --color-2: rgba(255, 0, 0, 0);
    --color-3: rgba(0, 255, 0, 0.8);
    --color-4: rgba(0, 255, 0, 0);
    --color-5: rgba(0, 0, 255, 0.8);
    --color-6: rgba(0, 0, 255, 0);
    --animation: mix-it-up;
    background: linear-gradient(217deg, var(--color-1), var(--color-2) 70.71%), linear-gradient(127deg, var(--color-3), var(--color-4) 70.71%), linear-gradient(336deg, var(--color-5), var(--color-6) 70.71%);
}

@keyframes mix-it-up {
    16.6% {
        --color-2: rgba(255, 0, 0, 0.8);
        --color-3: rgba(255, 0, 0, 0);
        --color-4: rgba(0, 255, 0, 0.8);
        --color-5: rgba(0, 255, 0, 0);
        --color-6: rgba(0, 0, 255, 0.8);
        --color-1: rgba(0, 0, 255, 0);
    }
    33.2% {
        --color-3: rgba(255, 0, 0, 0.8);
        --color-4: rgba(255, 0, 0, 0);
        --color-5: rgba(0, 255, 0, 0.8);
        --color-6: rgba(0, 255, 0, 0);
        --color-1: rgba(0, 0, 255, 0.8);
        --color-2: rgba(0, 0, 255, 0);
    }
    50% {
        --color-4: rgba(255, 0, 0, 0.8);
        --color-5: rgba(255, 0, 0, 0);
        --color-6: rgba(0, 255, 0, 0.8);
        --color-1: rgba(0, 255, 0, 0);
        --color-2: rgba(0, 0, 255, 0.8);
        --color-3: rgba(0, 0, 255, 0);
    }
    66.6% {
        --color-5: rgba(255, 0, 0, 0.8);
        --color-6: rgba(255, 0, 0, 0);
        --color-1: rgba(0, 255, 0, 0.8);
        --color-2: rgba(0, 255, 0, 0);
        --color-3: rgba(0, 0, 255, 0.8);
        --color-4: rgba(0, 0, 255, 0);
    }
    83.2% {
        --color-6: rgba(255, 0, 0, 0.8);
        --color-1: rgba(255, 0, 0, 0);
        --color-2: rgba(0, 255, 0, 0.8);
        --color-3: rgba(0, 255, 0, 0);
        --color-4: rgba(0, 0, 255, 0.8);
        --color-5: rgba(0, 0, 255, 0);
    }
}

Stripes

css
.seven {
    background: linear-gradient(90deg, transparent 50%, hsl(from var(--color-1) h s calc(l + 10) / alpha) 50%), linear-gradient(0deg, var(--color-1), var(--color-2));
    background-size: 50px 50px, auto;
    --animation: move;
}

@keyframes move {
  50% {
    --color-1: #E91E63;
    --color-2: #9c27b0;
  }
}

Checkers

css
.eight {
    --animation: reverse-colors;
    background: var(--color-2);
    background-image: linear-gradient(45deg, var(--color-1) 25%, transparent 25%), linear-gradient(-45deg, var(--color-1) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--color-1) 75%), linear-gradient(-45deg, transparent 75%, var(--color-1) 75%);
    background-size:40px 40px;
    background-position: 0 0, 0 20px, 20px -20px, -20px 0px;
}

@keyframes reverse-colors {
  50% {
    --color-1: #00bcd4;
    --color-2: #009688;
  }
}

The possibilities are endless

Other than the star movement, all of this is just gradient animation.

About the author