テクニック

CSSメッシュグラデーションの作り方|画像を使わない背景

この記事の対象: 実際に手を動かしているWeb制作者
読了時間: 約12分

写真やイラストを置かずに、色がにじみ合う背景を作る方法です。メッシュグラデーションは、複数の radial-gradient を重ねるだけで作れます。画像ファイルを1枚も読まないので、転送量が増えず、画面幅がどう変わっても破綻しません。背景だけで「ちゃんと作ってある」と感じさせたいキービジュアルや、ヒーローの下地に効きます。

ただし、文字を載せた瞬間に読めなくなる作り方が多い。色を足せば足すほど、どこが明るくてどこが暗いか自分で把握できなくなるためです。この記事では、そのまま動くCSSを全文載せたうえで、色の数と位置をどう決めるか、文字のコントラストをどう確保するか、動かす場合の負荷と prefers-reduced-motion の扱いまで書きます。


メッシュグラデーションとは何か、何を解決するのか

メッシュグラデーションとは何か、何を解決するのか

線形グラデーションでは出せない「にじみ」

linear-gradient は直線に沿って色が変わります。始点と終点が決まっているので、どうしても「帯」に見える。対してメッシュグラデーションは、キャンバスの上に色のついた光源をいくつか置いて、互いににじませたような見え方になります。Figma や Illustrator のメッシュツールで作る、あのふわっとした背景です。

これを画像で作ると、こうなります。

  • フルスクリーン背景だと 1920×1080 以上が必要になり、JPEGでも150KB前後、PNGだと数百KBになりやすい
  • Retina対応で2倍を用意すると、さらに倍
  • 画面比率が変わると、意図した色の位置がずれる
  • 色を少し変えたいだけで、デザインツールに戻って書き出し直し

CSSで作れば、全部なくなります。コードは1KB未満。色の変更はカラーコードを書き換えるだけ。縦長のスマホでも横長のデスクトップでも、% で位置を指定しておけば構図が保たれます。

画像背景とCSS背景の違い

CSSで作る

  • 転送量ほぼゼロ。追加リクエストなし
  • 色の修正はカラーコード1行
  • 画面比率が変わっても構図が崩れない
  • 表現できる形は円と楕円の重なりまで

画像を置く

  • 150KB〜数百KB。Retina対応で倍
  • 修正はデザインツールへ戻る
  • 比率が変わると色の位置がずれる
  • 自由な形・テクスチャが入れられる

図の通り、複雑な形やテクスチャが要るなら画像のほうが素直です。円の重なりで足りる表現なら、CSSで組んだほうが運用まで含めて楽になります。

仕組みは「透明に向かって消える円」を重ねること

肝は1つだけです。radial-gradient の終端を transparent にすると、円の外側が透けます。透けた部分から下のレイヤーが見えるので、重ねるほど色が混ざります。

/* 1つだけ置いた状態 */
background-image: radial-gradient(circle at 20% 30%, #ff6b9d 0%, transparent 50%);

これを , で区切って並べると、上に書いたものが前面に来ます。あとは置く位置と広がりを決めるだけです。


そのままコピーして動くメッシュグラデーション

そのままコピーして動くメッシュグラデーション

まずは完成形です。HTMLとCSSを別々に載せますが、片方だけでも成立します。

基本形:4色・静止

<section class="mesh-hero">
  <div class="mesh-hero__inner">
    <h1 class="mesh-hero__title">光の重なりで、<wbr>背景をつくる</h1>
    <p class="mesh-hero__lead">画像を1枚も使わずに、<wbr>色がにじみ合う背景を組み立てます。</p>
  </div>
</section>
.mesh-hero {
  position: relative;
  min-height: 70svh;
  display: grid;
  place-items: center;
  padding: 6rem 1.5rem;
  overflow: hidden;

  /* ベースの地色。グラデーションの隙間はここが見える */
  background-color: #1b1033;

  /* 奥から手前へ。最後に書いたものが一番手前に来る */
  background-image:
    radial-gradient(ellipse 70% 60% at 15% 20%, #4b2cc9 0%, transparent 60%),
    radial-gradient(ellipse 60% 55% at 85% 15%, #d6246e 0%, transparent 60%),
    radial-gradient(ellipse 65% 60% at 75% 85%, #1f6feb 0%, transparent 60%),
    radial-gradient(ellipse 55% 50% at 25% 90%, #2f9e8f 0%, transparent 60%);
  background-repeat: no-repeat;
}

.mesh-hero__inner {
  position: relative;
  z-index: 1;
  max-width: 44rem;
  text-align: center;
  color: #ffffff;
  word-break: keep-all;
  overflow-wrap: anywhere;
}

.mesh-hero__title {
  margin: 0 0 1.25rem;
  font-size: clamp(1.75rem, 5vw, 3rem);
  line-height: 1.35;
  letter-spacing: 0.01em;
}

.mesh-hero__lead {
  margin: 0;
  font-size: clamp(0.95rem, 2.2vw, 1.0625rem);
  line-height: 1.9;
  color: #e7e2f5;
}

これで、紫を地色に、左上に青紫、右上にピンク、右下に青、左下に青緑が浮かぶ背景になります。min-height: 70svh の svh は、スマホのアドレスバーが出ている状態の高さを基準にする単位です。vh だとスクロール時にアドレスバーが隠れて高さが変わり、背景の構図がわずかに動きます。

明るい地色でやる場合

暗い背景は色が映えますが、案件によっては使えません。明るい版も型は同じで、彩度を落として広がりを狭めます。

.mesh-light {
  position: relative;
  min-height: 60svh;
  display: grid;
  place-items: center;
  padding: 6rem 1.5rem;
  overflow: hidden;

  background-color: #f7f4ef;
  background-image:
    radial-gradient(ellipse 60% 50% at 10% 15%, #cfdcf5 0%, transparent 65%),
    radial-gradient(ellipse 55% 45% at 90% 20%, #f6dfe4 0%, transparent 65%),
    radial-gradient(ellipse 60% 50% at 80% 90%, #e3eee0 0%, transparent 65%);
  background-repeat: no-repeat;
}

.mesh-light__inner {
  position: relative;
  z-index: 1;
  max-width: 42rem;
  color: #26211c;      /* #f7f4ef に対して十分な差がある */
  word-break: keep-all;
  overflow-wrap: anywhere;
}

明るい地色のときは、彩度を上げると途端に安っぽくなります。各色のHSL彩度を 20〜35% あたりに収めると、紙に刷ったような落ち着きが出ます。ベージュ基調の配色をどこまで薄くできるかは、こちらの記事で実例ごとに見られます。


色の数と位置をどう決めるか

色の数と位置をどう決めるか

色は3〜5。6つ目から泥になる

試すと分かりますが、6色以上重ねた瞬間に色が互いを打ち消して、灰色がかった曖昧な面になります。混色の結果が予測できなくなるのも6色目あたりからです。

色数 見え方 向く用途
2色 単純な2トーン。メッシュに見えない ボタンや小さなカード
3色 構図が読める。最も扱いやすい コーポレート、医療、金融
4〜5色 豊かで奥行きが出る ブランドサイト、イベント、採用
6色以上 混ざって濁る。色が特定できない 使わない

3色と4色では難易度がかなり違います。迷ったら3色から始めて、物足りなければ1つ足す。この順番で進めると、どの色を足したら濁ったかが分かります。

位置は四隅+中央のどこかに置く

位置をランダムに散らすと、どこを見ればいいか分からない背景になります。実務では、置く場所をあらかじめ決めてしまったほうが速い。

色を置く位置の決め方
1. 主役を決めるブランドカラーを1色、視線の落ちる位置へ
2. 対角に置く2色目は主役の反対側。画面に張りが出る
3. 隙間を埋める3色目は残った辺へ。地色が覗く箇所を消す
4. 文字の位置を抜く文字が乗る領域だけ色を薄く、または避ける

4つ目の工程が抜けると、だいたい読めない背景になります。最初から文字の位置を決めて、そこを避けるように色を置くのが順番として正しい。

広がりは「はみ出させる」

radial-gradient の円を要素の内側に収めると、丸い玉がぽつんと置かれた見え方になります。意図した「にじみ」にするには、円の中心を枠の外か縁ぎりぎりに置いて、一部だけを見せます。

/* 枠の内側に収まっている。玉に見える */
background-image: radial-gradient(circle 200px at 50% 50%, #d6246e 0%, transparent 100%);

/* 枠の外から差し込む。にじみに見える */
background-image: radial-gradient(ellipse 80% 70% at -10% -20%, #d6246e 0%, transparent 60%);

at -10% -20% のように負の値を使えます。位置の指定は要素の左上を原点にした座標なので、マイナスにすると枠の外へ出ます。

transparent の罠:古いブラウザで灰色の線が出ることがある

transparent は仕様上 rgba(0, 0, 0, 0) です。つまり「透明な黒」。グラデーションの補間を素直にやると、色から透明へ向かう途中で黒が混ざり、境界に灰色のにじみが出ることがあります。

これを避ける書き方は、終端を「同じ色のアルファ0」にすることです。

/* 灰色が混じる可能性がある */
radial-gradient(ellipse 60% 50% at 20% 30%, #d6246e 0%, transparent 60%)

/* 同じ色のまま透明へ。混色の問題が起きない */
radial-gradient(ellipse 60% 50% at 20% 30%, rgb(214 36 110 / 1) 0%, rgb(214 36 110 / 0) 60%)

現行の主要ブラウザは premultiplied alpha で補間するので、transparent でも灰色は出にくくなっています。それでも、確実に意図した色で消したいなら後者の書き方が安全です。実案件では後者で書いておけば、検証の手間が1つ減ります。


文字を載せるときのコントラスト

文字を載せるときのコントラスト

背景が動く以上、固定の文字色は賭けになる

メッシュグラデーションの厄介なところは、背景の明るさが場所によって違う点です。文字を白にしても、下に明るい色が来ていればコントラストが足りません。WCAG 2.1 の AA は、本文(18px未満)で 4.5:1、大きな文字で 3:1 を要求します。背景のどこか1点で測って通しても、文字の端で割れていたら意味がない。

対策は3つあります。効き方と副作用が違います。

手法 コントラストの確保 見た目への影響 描画の負荷
オーバーレイを1枚敷く 確実。計算できる グラデーションが少し鈍る ほぼゼロ
文字側に影を付ける 不確実。斜めから光る 文字がぼやける 小
文字の背後だけ色を抜く 確実だが設計が重い 構図を作り直すことになる ほぼゼロ

実務では1つ目を使います。推奨は、文字が乗る領域に半透明の単色を1枚重ねること。

.mesh-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  /* 下に向かって濃くなる膜。文字の読みやすさを担保する */
  background: linear-gradient(
    to bottom,
    rgb(12 6 28 / 0.15) 0%,
    rgb(12 6 28 / 0.55) 100%
  );
  pointer-events: none;
}

この膜を敷くと、背景のどこに何色が来ていても、文字が乗る帯の明るさが一定の範囲に収まります。あとはその帯の実測値で文字色を決めればいい。

コントラストの測り方

目で見て判断しないこと。背景がグラデーションなので、ブラウザのスポイトで3点以上拾って、それぞれ計算します。

  1. Chrome DevTools で該当要素を選び、Styles の色見本からスポイトを起動する
  2. 文字の左端・中央・右端の背後の色を拾う
  3. 拾った値を Chrome の Color Picker に入れると、Contrast ratio が AA / AAA の判定付きで出る
  4. 最も明るい箇所で 4.5:1(大きな文字は 3:1)を満たすか確認する

DevTools には Rendering パネルの Emulate vision deficiencies もあります。Achromatopsia(全色盲)に切り替えると、色の差だけで作った区別が全部消えます。グラデーション背景は明度差が曖昧になりやすいので、ここで文字が沈んだら膜を濃くします。

配色そのものの判断軸は、黒を基調にした事例をまとめた記事が参考になります。暗い地色に文字を乗せる案件では、どこまで色を効かせるかの感覚がつかめます。

ボタンとリンクは背景に溶かさない

グラデーションの上にそのままボタンを置くと、縁が溶けます。フォーカスリングも背景色と近いと見えません。背景の上に置く操作要素は、不透明の面を持たせるのが確実です。

.mesh-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  padding: 0.9em 2em;
  border-radius: 999px;
  background-color: #ffffff;   /* 半透明にしない */
  color: #1b1033;
  font-weight: 700;
  text-decoration: none;
}

.mesh-cta:focus-visible {
  /* 背景がどんな色でも見えるよう、二重のリングにする */
  outline: 3px solid #ffffff;
  outline-offset: 3px;
  box-shadow: 0 0 0 6px rgb(0 0 0 / 0.55);
}

outline を白、その外側に黒い box-shadow を重ねると、明るい背景でも暗い背景でもリングが残ります。outline: none で消すのは論外ですが、背景色依存の単色リングも実質見えない場面が出ます。

キーボード操作で詰まりやすいのは、むしろグラデーションを乗せたナビゲーション側です。開閉するメニューのフォーカス管理は別記事で手順を追ってまとめています。


アニメーションさせる場合の負荷と prefers-reduced-motion

アニメーションさせる場合の負荷と prefers-reduced-motion

background-position を動かすと毎フレーム再描画される

色をゆっくり動かしたくなります。ただ、background-position や background-size のアニメーションは、合成だけで済みません。背景のグラデーションを毎フレーム描き直すので、要素が大きいほど重くなります。フルスクリーンのヒーローでやると、スクロールが引っかかる端末が出ます。

負荷を抑える書き方は、背景を持つ要素を別に切り出して transform だけで動かす方法です。transform と opacity は合成だけで処理されるので、再描画が走りません。

<section class="mesh-anim">
  <div class="mesh-anim__blobs" aria-hidden="true">
    <span class="mesh-anim__blob mesh-anim__blob--a"></span>
    <span class="mesh-anim__blob mesh-anim__blob--b"></span>
    <span class="mesh-anim__blob mesh-anim__blob--c"></span>
  </div>
  <div class="mesh-anim__inner">
    <h2 class="mesh-anim__title">動かすなら、<wbr>transform だけで動かす</h2>
  </div>
</section>
.mesh-anim {
  position: relative;
  min-height: 70svh;
  display: grid;
  place-items: center;
  padding: 6rem 1.5rem;
  overflow: hidden;
  background-color: #10132b;
  isolation: isolate;
}

.mesh-anim__blobs {
  position: absolute;
  inset: -20%;
  z-index: 0;
  filter: blur(80px);          /* ここが一番重い。値は控えめに */
  pointer-events: none;
}

.mesh-anim__blob {
  position: absolute;
  width: 55%;
  aspect-ratio: 1;
  border-radius: 50%;
  will-change: transform;
}

.mesh-anim__blob--a {
  top: 5%;
  left: 0%;
  background: #3b2bd4;
  animation: mesh-drift-a 18s ease-in-out infinite alternate;
}

.mesh-anim__blob--b {
  top: 0%;
  right: 0%;
  background: #c92a63;
  animation: mesh-drift-b 22s ease-in-out infinite alternate;
}

.mesh-anim__blob--c {
  bottom: 0%;
  left: 25%;
  background: #1a7f8c;
  animation: mesh-drift-c 26s ease-in-out infinite alternate;
}

@keyframes mesh-drift-a {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(12%, 8%, 0) scale(1.12); }
}

@keyframes mesh-drift-b {
  from { transform: translate3d(0, 0, 0) scale(1.05); }
  to   { transform: translate3d(-10%, 14%, 0) scale(0.95); }
}

@keyframes mesh-drift-c {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(8%, -10%, 0) scale(1.1); }
}

/* 文字の読みやすさを担保する膜 */
.mesh-anim::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(to bottom, rgb(8 6 20 / 0.2), rgb(8 6 20 / 0.6));
  pointer-events: none;
}

.mesh-anim__inner {
  position: relative;
  z-index: 1;
  max-width: 44rem;
  text-align: center;
  color: #ffffff;
  word-break: keep-all;
  overflow-wrap: anywhere;
}

.mesh-anim__title {
  margin: 0;
  font-size: clamp(1.5rem, 4.5vw, 2.5rem);
  line-height: 1.4;
}

/* 動きを減らす設定の人には止めて見せる */
@media (prefers-reduced-motion: reduce) {
  .mesh-anim__blob {
    animation: none;
    /* 止めた状態でも構図が成立する位置に置き直す */
    transform: translate3d(4%, 4%, 0) scale(1.05);
  }
}

blur の値がそのまま負荷になる

filter: blur() は、半径に応じて参照する画素が増えます。blur(80px) を画面いっぱいの要素にかけると、GPU メモリと描画時間を食います。実装するときの目安は次のとおりです。

  • ぼかす要素は画面の半分程度までに収める。全画面に blur をかけない
  • blur の半径は 60〜100px に収める。それ以上は見た目がほとんど変わらないのに重くなる
  • blur をかけた要素に will-change: transform を付ける。合成レイヤーに分離され、再描画を避けられる
  • 同じ画面内で backdrop-filter を併用しない。両方かけると低価格帯のスマホで目に見えて落ちる
  • 要素が画面外にあるセクションでは content-visibility: auto を併用して、描画自体を遅らせる

blur を使わずに済ませる手もあります。radial-gradient のグラデーション自体が十分なめらかなので、円を描いてぼかすのではなく、グラデーションを transform で動かす構成なら blur は不要です。.mesh-anim__blob の background を radial-gradient(circle, rgb(59 43 212 / 0.9) 0%, rgb(59 43 212 / 0) 70%) に変えて、filter: blur() を外してみてください。見た目はほぼ同じで、負荷は大きく下がります。この構成を推します。

prefers-reduced-motion は「止める」だけでは足りない

animation: none を書いて終わりにすると、キーフレームの開始位置で固まります。開始位置が構図として成立していないと、止めた人だけ変な背景を見ることになります。上のコードで transform を書き直しているのはこのためです。

動きを減らす設定は、前庭障害や片頭痛を持つ人が実際に使っています。省略していい分岐ではありません。確認方法は、Chrome DevTools の Rendering パネルで Emulate CSS prefers-reduced-motion を reduce にすると即座に切り替わります。macOS なら「システム設定 → アクセシビリティ → ディスプレイ → 視差効果を減らす」、Windows なら「設定 → アクセシビリティ → 視覚効果 → アニメーション効果」でも同じです。


ブラウザ対応と確認のしかた

ブラウザ対応と確認のしかた

radial-gradient 自体は現行の主要ブラウザで広く使えます。問題になるのは周辺の機能です。

使う機能 代替が必要になる場面
radial-gradient 複数重ね 現行の主要ブラウザで問題なし
rgb(R G B / A) のスラッシュ記法 古いブラウザ向けには rgba() で書く
svh / dvh 対応していない環境向けに vh をフォールバックとして先に書く
aspect-ratio 対応していない環境向けに padding-top: 100%
color-mix() 案件の対応範囲を確認してから使う
oklch() での指定 にじみは美しくなるが、未対応時は hsl() か16進数に落とす

対応状況はバージョンで変わるので、記憶や記事内の数字を鵜呑みにしないでください。確認は MDN の各ページ下部にある Browser compatibility 表か、caniuse.com で機能名を引くのが確実です。案件のアクセス解析でブラウザ構成を見て、対応範囲を先に決めてから書き始めると手戻りがありません。

フォールバックは、新しい記法より前に古い記法を書くだけで効きます。

.mesh-hero {
  min-height: 70vh;   /* 古いブラウザがこれを読む */
  min-height: 70svh;  /* 対応していればこちらで上書きされる */
}

対応していないブラウザは svh の行を解釈できずに捨てるので、上の値が残ります。@supports を書かなくてもこの順序で成立します。


案件の種類ごとに、使うか使わないか

案件の種類ごとに、使うか使わないか

「どれも良い」では判断に使えないので、言い切ります。

使ってよい

ブランドサイト・イベント・サービスのLP。背景で世界観を出す必要があり、情報量が少ないページ。メッシュグラデーションの曖昧さが邪魔にならず、画像を使わずに雰囲気を作れる利点が効きます。1画面で完結するレイアウトとは相性が良い。

SaaS・ツール系のサービスサイト。「技術的に新しい」という印象を背景だけで出せます。ダッシュボードのスクリーンショットを前面に置くとき、下地に使うと画面が締まります。

採用サイトのキービジュアル。写真素材が揃っていない段階でも、背景だけで印象を作れます。ただし採用サイトは人の顔が効く領域なので、写真が用意できるならそちらを優先します。

条件付きで使う

コーポレートサイト。ヒーロー1箇所に限定し、彩度を落とすこと。全面に広げると業種を問わず軽く見えます。信頼を示す必要がある業種では、背景のにじみよりも情報の整理が効きます。

EC。商品写真の背後では使わない。色が干渉して商品の色が正しく伝わりません。使えるのはキャンペーンのバナーや会員登録の導線まわりに限ります。

使わない

医療・金融・法律・公共。背景の明るさが場所によって変わる作りは、文字の読みやすさの保証が難しい。この領域は、コントラストを単色で確定させるほうが正しい。業種別の実例を見ると、この領域が単色背景に寄っているのが分かります。

文字量の多い記事ページ・資料ページ。長文の背後にグラデーションを置くと、読み進めるうちに文字が沈む箇所が必ず出ます。本文の背景は単色にする。

ブランドカラーが2色以下に厳格に決まっている案件。メッシュグラデーションは混色で中間色が生まれます。ブランドガイドラインで使用色が限定されている場合、生まれた中間色が規定違反になります。レギュレーションを確認せずに提案しないこと。

流行っているから入れる、という判断はしません。背景で世界観を担保する必要があるかどうかで決めます。必要がなければ、単色のほうが速くて読みやすい。


よくある質問

よくある質問

CSSのメッシュグラデーションは重いですか

静止した状態なら、ほぼ負荷はありません。radial-gradient を4つ重ねても描画は1回で終わり、画像リクエストも発生しないので、画像背景より軽くなります。重くなるのは filter: blur() を大きな要素にかけたときと、background-position をアニメーションさせたときです。動かす場合は transform と opacity だけに限定してください。

色は何色まで重ねていいですか

3〜5色に収めます。6色以上重ねると互いに打ち消し合って、灰色がかった曖昧な面になります。3色が最も扱いやすく、構図が読める状態を保てます。迷ったら3色から始めて、必要に応じて1色ずつ足すと、どこで濁ったかが分かります。

グラデーション背景に文字を載せると読みにくくなります

背景の明るさが場所によって違うためです。文字が乗る領域に半透明の単色を1枚重ねて、明るさの幅を狭めてから文字色を決めてください。確認は Chrome DevTools のスポイトで、文字の左端・中央・右端の背後3点を拾い、それぞれコントラスト比を測ります。本文は 4.5:1、大きな文字は 3:1 が基準です。

prefers-reduced-motion はどう書けばいいですか

@media (prefers-reduced-motion: reduce) の中で animation: none にします。ただしそれだけでは、キーフレームの開始位置で固まって構図が崩れることがあります。止めた状態でも成立する位置に transform を書き直してください。動作確認は Chrome DevTools の Rendering パネルにある Emulate CSS prefers-reduced-motion で切り替えられます。

transparent で終わらせると境界が灰色になることがあるのはなぜですか

transparent は仕様上「透明な黒」(rgba(0,0,0,0))だからです。色から透明へ補間する途中で黒が混ざり、灰色のにじみが見える場合があります。終端を rgb(214 36 110 / 0) のように、同じ色のアルファ0で書くと確実に回避できます。現行ブラウザでは起きにくくなっていますが、こちらで書いておけば検証が1つ減ります。


まとめ

メッシュグラデーションは、radial-gradient を3〜5個重ねて終端を透明にするだけで作れます。画像を使わないので転送量が増えず、画面幅が変わっても構図が保てる。これが最大の利点です。

実装で押さえるのは4点です。

  1. 色は3〜5。位置は四隅と中央から選ぶ。文字が乗る領域を先に決めて、そこを避けて色を置く
  2. 文字の背後に半透明の膜を1枚敷く。背景の明るさの幅を狭めてから、実測でコントラストを確認する
  3. 動かすなら transform と opacity だけ。background-position のアニメーションと大きな blur は再描画の負荷になる
  4. prefers-reduced-motion で止めるときは、止めた状態の構図まで作る

そして、使う案件と使わない案件を分けること。ブランドサイト・SaaS・イベントでは効きます。医療・金融・公共、そして文字量の多いページでは、単色のほうが正しい。背景の表現力が必要かどうかで判断してください。