CSSで、背景のグラデーションをゆっくり動かす方法
背景のグラデーションをゆっくり流れるように動かすには、グラデーションを background-size で要素の3倍の幅に描き、見える位置を background-position で少しずつずらします。JavaScript は要りません。background-size を書き忘れると位置を変えても色が動かないことと、alternate を付けないと1周ごとに色が飛ぶことが、つまずきやすい点です。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、Web Animations API でアニメーションを止めて時刻を変えては、background-position などの計算値と、要素の左端・中央・右端の画素の色を読みました。動きを減らす設定は、Playwright の emulateMedia() で再現しています。Firefox と Safari、スマートフォンの実機は試していません。動かしている間の CPU の使用量など、描画の負荷も測っていません。
グラデーションを3倍の幅にして、位置を動かす
水色から紫、ピンクへ変わるグラデーションを、片道10秒で左右に行き来させる CSS です。
.gradient-bg {
background-image: linear-gradient(90deg, #00c8e8, #9b7cf0, #ff8fab);
background-size: 300% 100%;
animation: gradient-shift 10s ease-in-out infinite alternate;
}
@keyframes gradient-shift {
from {
background-position: 0% 50%;
}
to {
background-position: 100% 50%;
}
}
<div class="gradient-bg"></div>
background-size: 300% 100% で、グラデーションを要素の3倍の幅に描きます。要素の中に表示されるのは、そのうち3分の1の幅だけです。background-position の横の値を 0% から 100% へ変えると、見える範囲がグラデーションの左端から右端へ移ります。

幅 400px、高さ 200px の要素で、要素の中央の画素の色を読みました。0秒では水色寄りの #33aeea、5秒では紫の #9b7cef、10秒ではピンク寄りの #dd88c1 でした。そのあとは同じ色を逆の順にたどり、20秒の時点で #33aeea に戻ります。
ease-in-out を指定しているので、動きは端でゆっくり、中ほどで速くなります。2.5秒の時点の background-position は 12.9162% で、片道の4分の1の時間がたっても、まだ1割ほどしか進んでいませんでした。もっとゆっくり動かしたいときは、animation の 10s を 20s のように長くします。
background-size を書かないと、位置を変えても色は動かない
上の CSS から background-size の行だけを消すと、background-position の計算値は 0% 50%、50% 50%、100% 50% と変わっているのに、画面は少しも動きません。0秒、5秒、10秒のどの時点でも、左端・中央・右端の色は #0fc0e8、#9b7cef、#f58db1 のままでした。
理由は、パーセント値の位置の決まり方にあります。MDN の background-position のページによると、パーセント値は、コンテナーの寸法から背景画像の寸法を引いた値に掛けて、ずらす量を決めます。そして、画像とコンテナーの寸法が同じなら、その差がゼロになるので、パーセント値で位置を指定しても効果がありません。
グラデーションには、画像ファイルのような決まった大きさがありません。background-size が既定の auto のままだと、背景を置く領域(既定では要素のパディングボックス)と同じ大きさで描かれます。そのため、要素とグラデーションの幅の差がゼロになり、どのパーセント値でも同じ見え方になります。300% のように要素より大きくしておくと、幅 400px の要素では差が 800px になり、100% で 800px 左にずれます。
alternate を外すと、1周ごとに色が飛ぶ
animation の最後の alternate は、animation-direction の値です。周期ごとに再生の向きを反転させます。これを外すと、毎回 0% から 100% へ進んだ後、次の周期でいきなり 0% に戻ります。
| アニメーションの時刻 | alternate なしの位置 |
中央の色 | alternate ありの位置 |
中央の色 |
|---|---|---|---|---|
| 9.99秒 | 99.9998% |
#dd88c1 |
99.9998% |
#dd88c1 |
| 10.00秒 | 0% |
#33aeea |
100% |
#dd88c1 |
| 10.10秒 | 0.018982% |
#33aeea |
99.981% |
#dd88c1 |
alternate がないと、10秒ちょうどで中央の色がピンク寄りから水色寄りに一瞬で変わりました。ゆっくり動いていた背景が、周期の終わりにぱっと切り替わります。alternate を付けると、10秒の時点で向きが変わるだけで、色は続いたままでした。
background-image を @keyframes で直接変えても、なめらかには変わらない
グラデーションの色を変えたいとき、@keyframes の中で background-image に別のグラデーションを書く方法も考えられます。
/* うまくいかない書き方 */
@keyframes image-swap {
from {
background-image: linear-gradient(90deg, #00c8e8, #9b7cf0);
}
to {
background-image: linear-gradient(90deg, #9b7cf0, #ff8fab);
}
}
検証環境で、これを10秒かけて動かしました。4.9秒までは最初のグラデーションのままで、4.9秒と5秒のあいだで後のグラデーションに切り替わりました。途中の色にはならず、片道の10秒のうちでは、半分の時点で一度だけ切り替わる動きです。
色そのものを変えるときは、@property で色を登録する
位置を動かすのではなく、グラデーションの色そのものを少しずつ変えたいときは、色をカスタムプロパティに入れ、@property で「色」として登録してから動かします。
@property --from {
syntax: "<color>";
inherits: false;
initial-value: #00c8e8;
}
@property --to {
syntax: "<color>";
inherits: false;
initial-value: #9b7cf0;
}
.color-shift {
background-image: linear-gradient(90deg, var(--from), var(--to));
animation: color-shift 10s ease-in-out infinite alternate;
}
@keyframes color-shift {
to {
--from: #9b7cf0;
--to: #ff8fab;
}
}
--from の計算値は、0秒で rgb(0, 200, 232)、2.5秒で rgb(20, 190, 233)、5秒で rgb(78, 162, 236)、7.5秒で rgb(135, 134, 239)、10秒で rgb(155, 124, 240) と、少しずつ変わりました。要素の左端の画素の色も、#07c4e8 から #a07cec へ順に変わっています。
@property のルールを書かずに同じことをすると、カスタムプロパティの値は、4.9秒と5秒のあいだで切り替わるだけでした。background-image を直接変えたときと同じ動きです。
この方法なら background-size を大きくする必要はありません。グラデーションの角度を回したいときも、同じように @property で <angle> として登録します。その例は、別の記事「CSSで、枠線が回りながら光るボタンを作る方法」で扱っています。
動きを減らす設定と、一時停止のボタン
OS で動きを減らす設定にしている人には、prefers-reduced-motion のメディアクエリでアニメーションを外します。
@media (prefers-reduced-motion: reduce) {
.gradient-bg {
animation: none;
}
}
Playwright で reducedMotion: 'reduce' にして開くと、2秒後も animation-name は none で、アニメーションの数は0でした。background-position は既定の 0% 0% になり、アニメーションの0秒の時点と同じ横位置で止まったままです。
その場で止められるボタンを置くときは、animation-play-state を paused にするクラスを用意し、ボタンで付け外しします。
.gradient-bg.is-paused {
animation-play-state: paused;
}
<div id="box" class="gradient-bg"></div>
<button id="btn">一時停止</button>
<script>
const btn = document.getElementById('btn');
const box = document.getElementById('box');
btn.addEventListener('click', () => {
const paused = box.classList.toggle('is-paused');
btn.textContent = paused ? '再生' : '一時停止';
});
</script>

実際の時間で試すと、ページを開いて2秒後に is-paused を付けたところで、background-position は 8.306% で止まりました。2秒待っても同じ値のままで、アニメーションの時刻も 2017ms から進みませんでした。クラスを外すと続きから動き出し、2秒後には時刻が 4033ms、位置が 33.7192% になりました。止めていた間の分は進んでおらず、止めた位置から再開しています。
使ったプロパティの対応状況
MDN の互換性データ(@mdn/browser-compat-data 8.1.8)では、background-size は Chrome 3、Firefox 4、Safari 5 から使えます。animation-direction と animation-play-state は Chrome 43、Firefox 16、Safari 9 から、prefers-reduced-motion は Chrome 74、Firefox 63、Safari 10.1 からです。
いちばん新しいのは、色そのものを動かす方法で使う @property で、Chrome 85、Firefox 128、Safari 16.4 からです。位置を動かす方法は @property を使わないので、互換性データの上では、@property に対応する前のブラウザでも使える組み合わせです。





