画像を丸く切り抜いたり、写真の下端をふわっと消したり、文字の形で背景を覗かせたり——こうした「形に沿った切り抜き」を CSS だけで実現できるのが mask-image です。clip-path と似ていますが、境界をぼかしたり半透明にできる点が大きく違います。この記事では mask-image の考え方から、グラデーションによるフェードアウト、SVG での切り抜き、複数マスクの合成までを、実際に動かせるコード付きで解説します。
目次
mask-image は「マスク画像の不透明度」で要素を透かす
マスクとは、要素に重ねる型紙のような画像のことです。mask-image にその画像を指定すると、ブラウザーはマスク画像のアルファ値(不透明度)をピクセルごとに読み取り、その値をそのまま要素の見え方に反映します。マスク画像が不透明なところでは要素がそのまま見え、透明なところでは要素が消え、半透明なところでは要素も半透明になります。
ここでポイントになるのは、既定のモード(アルファ)ではマスク画像の「色」は結果に関係しないということです。黒で描こうが赤で描こうが、不透明であれば要素は同じように見えます。だから後で紹介するグラデーションのマスクも、black から transparent へ変化させて「不透明から透明へ」を作っているだけで、黒という色そのものに意味はありません。
clip-path との違いもここにあります。clip-path は多角形や円のパスで要素をパキッと切り取るプロパティで、切り取る/切り取らないの二択です。一方 mask-image は不透明度という連続した値を扱うので、ぼかした境界やだんだん薄くなる表現ができます。単純な図形にきっちり切り抜くだけなら clip-path、フェードや複雑な形、画像の形を使いたいときは mask-image と考えると選びやすくなります。
基本の使い方|透過画像を型紙にする
もっとも素直な使い方は、透過 PNG や SVG をそのまま型紙にする方法です。mask-image に url() で画像を指定します。マスク画像は既定ではタイル状に繰り返されるので、ひとつの形で切り抜きたいときは mask-repeat: no-repeat と、大きさを決める mask-size をあわせて指定します。
.thumb {
width: 300px;
height: 200px;
background: url(/images/photo.jpg) center / cover;
/* star.png の不透明な部分だけが残る */
mask-image: url(/images/star.png);
mask-repeat: no-repeat; /* 繰り返さない */
mask-position: center; /* 中央に置く(初期値も center) */
mask-size: contain; /* 要素に収まるサイズに合わせる */
}
/* ショートハンドならこの1行でも同じ意味になる */
.thumb-short {
mask: url(/images/star.png) center / contain no-repeat;
}
気づいた方もいるかもしれませんが、mask-repeat や mask-position といったサブプロパティの並びは background-* とそっくりです。実際、値の書き方も background-* とほぼ同じ考え方で通用します。「背景画像を敷くのと同じ感覚で型紙を敷く」と覚えておけば、細かい値をいちいち調べ直さずに書けます。
| プロパティ | 役割 | 初期値 |
|---|---|---|
mask-image | マスクとして使う画像やグラデーションを指定する | none |
mask-mode | アルファ値で見るか、明るさ(輝度)で見るかを決める | match-source |
mask-repeat | マスク画像を繰り返すかどうか | repeat |
mask-position | マスク画像を置く位置 | center |
mask-size | マスク画像の大きさ(cover / contain / 数値) | auto |
mask-origin | 位置の基準になるボックス | border-box |
mask-clip | マスクの影響が及ぶ範囲のボックス | border-box |
mask-composite | マスクを複数重ねたときの合成方法 | add |
mask | 上記すべてをまとめて書くショートハンド | — |
background-* と似ているとはいえ、初期値が違うものがあります。background-position の初期値は左上(0% 0%)ですが mask-position は center、background-origin は padding-box ですが mask-origin は border-box です。位置がずれて見えるときは、この差を疑ってください。
グラデーションでフェードアウトさせる
mask-image には画像だけでなく linear-gradient() や radial-gradient() も指定できます。CSS のグラデーションは画像として扱われるので、そのままマスクになるのです。これがおそらくもっとも出番の多い使い方で、画像を1枚も用意せずに「下に向かってだんだん消えていく」表現が作れます。
mask-image: linear-gradient(to bottom, black, transparent) と書けば、上端が不透明・下端が透明のマスクになり、要素は下へ向かってフェードアウトします。中央だけを残したいときは radial-gradient() で円形のマスクを作ります。3つ並べて見比べてみてください。
<div class="grid">
<figure class="cell">
<div class="box"></div>
<figcaption>マスクなし</figcaption>
</figure>
<figure class="cell">
<div class="box fade-bottom"></div>
<figcaption>下へフェード</figcaption>
</figure>
<figure class="cell">
<div class="box fade-circle"></div>
<figcaption>中央の円だけ残す</figcaption>
</figure>
</div>
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
font-family: sans-serif;
}
.cell {
margin: 0;
}
.cell figcaption {
margin-top: 6px;
font-size: 12px;
color: #555;
text-align: center;
}
/* マスクをかける対象。外部画像は使わず CSS の背景で描いている */
.box {
height: 110px;
border-radius: 6px;
background: linear-gradient(135deg, #4a90d9, #8e44ad);
}
/* 上は不透明(black)、下は透明(transparent)→ 下へ向かって消えていく */
.fade-bottom {
-webkit-mask-image: linear-gradient(to bottom, black, transparent);
mask-image: linear-gradient(to bottom, black, transparent);
}
/* 中央だけ不透明にして、外側へ向かって消していく */
.fade-circle {
-webkit-mask-image: radial-gradient(circle at center, black 35%, transparent 70%);
mask-image: radial-gradient(circle at center, black 35%, transparent 70%);
}
グラデーションのマスクは色の停止位置を調整するだけで効果を細かく変えられます。linear-gradient(to bottom, black 60%, transparent) のように書けば「上から6割は完全に見えて、そこから下だけ薄くなる」といった具合です。長い文章の続きを示す「もっと読む」の演出や、画像の端をなじませたいときによく使います。
なお opacity との違いも押さえておきましょう。opacity は要素全体を一律に薄くしますが、マスクは場所ごとに濃さを変えられるのが強みです。
mask-mode で白黒の SVG を型紙にする
mask-mode は、マスク画像の何を読み取るかを切り替えるプロパティです。alpha なら不透明度、luminance なら明るさ(輝度)を見ます。luminance では白いところが「見える」、黒いところが「消える」という扱いになるので、透過を持たない白黒の画像でも型紙として使えます。
初期値は match-source で、これは「マスク画像の種類に合わせる」という意味です。PNG や CSS グラデーションなど普通の画像なら alpha、SVG の <mask> 要素を参照した場合は luminance として扱われます。ここを知らないと、次のデモの真ん中のように「切り抜かれず全面が残る」現象にぶつかります。
下のデモでは同じ星の形を3通りで指定しています。左は星の外側が透明な SVG なので alpha でそのまま切り抜かれます。真ん中は背景を黒で塗りつぶした SVG を alpha で使った失敗例で、全面が不透明なため何も切り抜かれません。右は同じ SVG を luminance にしたもので、白い星の部分だけが残ります。
<div class="grid">
<figure class="cell">
<div class="box star-alpha"></div>
<figcaption>透過SVG+alpha</figcaption>
</figure>
<figure class="cell">
<div class="box star-wrong"></div>
<figcaption>白黒SVG+alpha(失敗)</figcaption>
</figure>
<figure class="cell">
<div class="box star-luminance"></div>
<figcaption>白黒SVG+luminance</figcaption>
</figure>
</div>
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
font-family: sans-serif;
}
.cell { margin: 0; }
.cell figcaption {
margin-top: 6px;
font-size: 12px;
color: #555;
text-align: center;
}
.box {
height: 110px;
background: linear-gradient(135deg, #f6c343, #e8542f);
/* 3つとも共通の指定。background-* と同じ考え方 */
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-size: 90% 90%;
mask-size: 90% 90%;
}
/* 星の外側が「透明」な SVG。alpha モードなら星型に切り抜かれる */
.star-alpha {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpolygon points='50,4 63,37 98,38 70,60 80,95 50,74 20,95 30,60 2,38 37,37' fill='black'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpolygon points='50,4 63,37 98,38 70,60 80,95 50,74 20,95 30,60 2,38 37,37' fill='black'/%3E%3C/svg%3E");
-webkit-mask-mode: alpha;
mask-mode: alpha;
}
/* 背景が黒で塗りつぶされた SVG。全面が不透明なので alpha では切り抜かれない */
.star-wrong {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='black'/%3E%3Cpolygon points='50,4 63,37 98,38 70,60 80,95 50,74 20,95 30,60 2,38 37,37' fill='white'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='black'/%3E%3Cpolygon points='50,4 63,37 98,38 70,60 80,95 50,74 20,95 30,60 2,38 37,37' fill='white'/%3E%3C/svg%3E");
mask-mode: alpha;
}
/* 同じ SVG を luminance にすると、白い部分だけが残る */
.star-luminance {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='black'/%3E%3Cpolygon points='50,4 63,37 98,38 70,60 80,95 50,74 20,95 30,60 2,38 37,37' fill='white'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='black'/%3E%3Cpolygon points='50,4 63,37 98,38 70,60 80,95 50,74 20,95 30,60 2,38 37,37' fill='white'/%3E%3C/svg%3E");
mask-mode: luminance;
}
デモでは SVG を data URI(url("data:image/svg+xml,..."))で直接埋め込んでいます。外部ファイルを用意しなくてよいので、アイコンの形で切り抜くような小さな用途には便利な書き方です。ただし data URI の中で # をそのまま書くとフラグメント(アンカー)の開始と解釈されて画像が壊れるため、fill="#fff" のような色指定は %23fff にエスケープするか、white のような色キーワードを使ってください。
SVG ファイル側で <mask> 要素を定義しておき、mask-image: url(mask.svg#myMask) のように id で参照する方法もあります。この場合は前述のとおり輝度で解釈されるので、マスク内の図形は白で塗っておきます。
複数のマスクを重ねて mask-composite で形を作る
mask-image はカンマ区切りで複数のマスクを指定できます。そして、それらをどう合成するかを決めるのが mask-composite です。これを使うと「丸から丸を引いてドーナツにする」「重なった部分だけを残す」といった、単独のグラデーションでは作れない形を組み立てられます。
合成の順番を理解するには、レイヤーの上下関係を押さえる必要があります。カンマ区切りで先に書いたものが上の層で、mask-composite は「その層」と「それより下にある層をまとめた結果」をどう合成するかを指定します。つまり2枚の場合、1枚目が引く側、2枚目が引かれる側になります。
| 値 | 合成のしかた |
|---|---|
add | 上の層を下の層に足し合わせる。どちらかに含まれる範囲が残る(初期値) |
subtract | 上の層のうち、下の層と重なっていない部分だけが残る |
intersect | 上の層と下の層が重なっている部分だけが残る |
exclude | 重なっていない部分だけが残る(重なりが抜ける) |
言葉だけだと分かりにくいので、左右にずらした同じ大きさの円を2枚重ね、値だけを変えて見比べてみます。subtract では左の円から重なりが削られて三日月になり、exclude では重なった部分が抜けてリング状の穴があきます。
<div class="grid">
<figure class="cell">
<div class="box add"></div>
<figcaption>add(重ねる)</figcaption>
</figure>
<figure class="cell">
<div class="box subtract"></div>
<figcaption>subtract(引く)</figcaption>
</figure>
<figure class="cell">
<div class="box intersect"></div>
<figcaption>intersect(重なりだけ)</figcaption>
</figure>
<figure class="cell">
<div class="box exclude"></div>
<figcaption>exclude(重なりを除く)</figcaption>
</figure>
</div>
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
font-family: sans-serif;
}
.cell { margin: 0; }
.cell figcaption {
margin-top: 6px;
font-size: 12px;
color: #555;
text-align: center;
}
.box {
height: 100px;
background: linear-gradient(135deg, #ff8a3d, #d64550);
/* 2枚のマスクを重ねる。左の円が「上」の層、右の円が「下」の層 */
-webkit-mask-image:
radial-gradient(circle 34px at calc(50% - 18px) 50%, black 98%, transparent 100%),
radial-gradient(circle 34px at calc(50% + 18px) 50%, black 98%, transparent 100%);
mask-image:
radial-gradient(circle 34px at calc(50% - 18px) 50%, black 98%, transparent 100%),
radial-gradient(circle 34px at calc(50% + 18px) 50%, black 98%, transparent 100%);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
}
/* 上の層を下の層に足す=2つの円が合体した形 */
.add { mask-composite: add; }
/* 上の層から下の層の形を削る=左の円が三日月のように欠ける */
.subtract { mask-composite: subtract; }
/* 重なっている部分だけが残る */
.intersect { mask-composite: intersect; }
/* 重なっている部分だけが消える */
.exclude { mask-composite: exclude; }
subtract を応用すると、要素にきれいな穴をあけられます。たとえば大きい円のマスクから小さい円のマスクを引けばドーナツ型になり、背景に敷いた画像や色が穴から透けて見えます。border-radius や擬似要素では作れない形を、要素1つで表現できるのが面白いところです。
.donut {
width: 160px;
height: 160px;
background: linear-gradient(135deg, #4a90d9, #8e44ad);
mask-image:
/* 1枚目(上の層): 外側の大きい円 */
radial-gradient(circle 80px at center, black 98%, transparent 100%),
/* 2枚目(下の層): 内側の小さい円 */
radial-gradient(circle 40px at center, black 98%, transparent 100%);
mask-repeat: no-repeat;
mask-composite: subtract; /* 大きい円から小さい円を引く */
}
-webkit- 接頭辞はどこまで必要か
mask-image は現在の主要ブラウザーで接頭辞なしに使えますが、そこに至るまでの経緯があるためコードでは -webkit-mask-image をあわせて書くことがよくあります。Firefox は以前から接頭辞なしで対応していた一方、Chromium 系(Chrome / Edge)は長い間 -webkit- 付きの記述だけを解釈していて、接頭辞なしの mask-* と mask ショートハンドに対応したのは Chrome 120(2023年12月)からです。Safari も歴史的に -webkit- 付きで実装されてきました。
そのため、少し前の端末やブラウザーまで含めて確実に動かしたいなら、-webkit-mask-image を先に書き、標準の mask-image を後に書くのが基本の形です。後に書いた標準プロパティが優先されるので、対応済みのブラウザーでは標準の記述が使われます。Autoprefixer などのツールを使っているなら、標準プロパティだけ書けば自動で補完されます。
.fade {
/* 接頭辞つきを先に、標準を後に書く */
-webkit-mask-image: linear-gradient(to bottom, black, transparent);
mask-image: linear-gradient(to bottom, black, transparent);
}
ひとつ注意したいのが mask-composite です。-webkit-mask-composite は source-out や xor といった別の値を取る旧仕様のプロパティで、標準の add / subtract などをそのまま書いても解釈されません。単純に接頭辞を付け足せば済むわけではないので、合成を使うときは接頭辞なしの mask-composite に対応した環境を前提にするか、@supports (mask-composite: subtract) で分岐させてください。
マスクが効かない・要素ごと消えてしまうとき
要素が丸ごと見えなくなる
マスクを指定した瞬間に要素がまるごと消えたときは、マスク画像に不透明な部分がひとつもない状態になっていないかを確認します。画像のパスが間違っている、data URI のエスケープが足りず画像として解釈されていない、といったケースでこうなりがちです。開発者ツールのネットワークタブでマスク画像が実際に読み込まれているか、Styles パネルで mask-image の値が無効になっていないかを見てください。
切り分けの近道は、いったん mask-image: linear-gradient(black, black) に置き換えてみることです。全面が不透明なマスクなので要素は元どおり見えるはずで、これで見えるならマスク画像側の問題、それでも消えるなら別の原因だと判断できます。
マスクが小さくタイル状に並んでしまう
mask-repeat の初期値は repeat です。background-image と同じで、マスク画像が要素より小さいと格子状に敷き詰められます。ひとつの形で切り抜きたいときは mask-repeat: no-repeat を忘れずに書き、あわせて mask-size: contain や mask-size: 100% 100% で大きさを決めます。
黒く塗った画像を指定したのに切り抜かれない
これは mask-mode の勘違いです。既定のアルファモードで見ているのは不透明度だけなので、白地に黒で描いた画像は「全面が不透明」=すべて表示、という結果になります。白黒で描いた図をそのまま型紙にしたいなら mask-mode: luminance を指定するか、切り抜きたい形の外側が透明な画像を用意し直してください。
あとから書いた mask ショートハンドで設定が消える
ショートハンドの mask は、省略したサブプロパティをすべて初期値に戻します。mask-composite: subtract を書いたあとに mask: url(...) と書くと、mask-composite は add に戻ってしまいます。メディアクエリや別のセレクターで一部だけ上書きしたいときは、ショートハンドではなく mask-image などの個別プロパティを使うほうが安全です。
文字や子要素まで一緒に薄くなる
マスクは指定した要素の描画全体、つまり背景・ボーダー・中のテキストや子要素すべてに効きます。「画像だけフェードさせて、上に載せたテキストははっきり見せたい」という場合は、マスクをかける専用の要素(背景を持つ子要素や擬似要素)を用意し、テキストはその外側か上に重ねる構成にします。
まとめ
mask-image は、指定した画像の不透明度に応じて要素を透かすプロパティです。不透明なところは見え、透明なところは消え、その中間は半透明になります。だから clip-path のようなきっぱりした切り抜きだけでなく、境界をぼかしたフェード表現もできます。
書き方は background-* とほぼ同じで、mask-repeat / mask-position / mask-size で敷き方を整えます。初期値が repeat であることと、mask-mode の alpha と luminance の違いを押さえておけば、「効かない」「消える」といったつまずきはほとんど避けられます。複数のマスクと mask-composite を組み合わせれば穴あきの形も作れるので、まずはグラデーションのフェードから試してみてください。