CSSのmask-imageで、画像や一覧の端をふわっと消す方法
画像や一覧の端をだんだん透明にするには、CSS の mask-image に、黒から透明へ変わるグラデーションを指定します。マスクの不透明な所は表示され、透明な所は消えます。白いグラデーションを重ねる方法と違って後ろの背景が透けるので、背景が模様や写真でも、つなぎ目が目立ちません。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。白い背景の上に黒い箱を置いてマスクをかけ、画面を撮って、画素の明るさから不透明度を読みました。Firefox と Safari での表示は試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。
画像の下をふわっと消す
画像の下半分を、下に行くほど透明にする例です。
<img class="photo" src="photo.svg" alt="">
.photo {
mask-image: linear-gradient(to bottom, #000 50%, transparent);
}
グラデーションの #000 50% は「上から 50% までは黒(表示する)」、最後の transparent は「下の端で透明(消す)」という意味です。検証環境で、高さ 200px の黒い箱にこのマスクをかけて不透明度を測ると、上から 99px までは 1 のままで、125px で 0.75、150px で 0.49、175px で 0.24、いちばん下で 0 になりました。
マスクの色は関係ありません。効くのは、色の不透明度だけです。linear-gradient(#fff, #fff) や linear-gradient(red, red) のような不透明な色のマスクでは、箱はそのまま表示されました。不透明度が 0.5 の色なら、青でも白でも、箱の不透明度は 0.5 になりました。MDN の mask-image のページにあるとおり、mask-mode の初期値は match-source で、グラデーションのような画像では、不透明度(アルファ)でマスクされます。この記事では、表示する所を #000、消す所を transparent と書いています。
白いグラデーションを重ねる方法との違い
端を消す方法には、画像の上に、透明から白へ変わるグラデーションを重ねるやり方もあります。この方法は、白いグラデーションで画像を隠しているだけです。後ろの背景が白でないと、白い帯が見えます。

mask-image は画像そのものを透明にするので、上の画像の下側の例のように、背景の水玉がそのまま透けて見えます。
一覧の左右の端を消す
横にスクロールする一覧の端を消すときは、スクロールする枠に mask-image を付けます。消す幅は、% ではなく px で書くと、枠の幅が変わっても同じ幅になります。
.list {
overflow-x: auto;
mask-image: linear-gradient(
to right,
transparent,
#000 60px,
#000 calc(100% - 60px),
transparent
);
}
検証環境で、消す幅を 40px にして、幅 300px と 600px の箱で比べました。#000 40px と書いた箱は、どちらの幅でも、左端から 39px の位置で不透明度が 0.99 以上になりました。#000 15% と書いた箱では、その位置が 44px と 89px になり、幅に合わせて消える幅も変わりました。
マスクは枠に付いたまま動きません。幅 400px の枠で scrollLeft を 0、200、600 と変えても、不透明度は、左端から 30px の位置で 0.51、60px の位置で 1、右端から 30px の位置で 0.49 と、どのスクロール位置でも同じでした。中身がスクロールしても、消えるのは常に枠の両端です。
4辺を消すときは mask-composite: intersect
画像の上下左右の4辺を消すには、横向きと縦向きのグラデーションを、カンマで区切って2つ書きます。このとき、mask-composite: intersect も書きます。
.photo {
mask-image:
linear-gradient(to right, transparent, #000 20%, #000 80%, transparent),
linear-gradient(to bottom, transparent, #000 20%, #000 80%, transparent);
mask-composite: intersect;
}

MDN の mask-composite のページにあるとおり、初期値の add は、マスクを上に重ねていきます。横のマスクで消した左の辺も、縦のマスクでは表示する所なので、重ねると表示されてしまいます。検証環境で、200px の箱に、端から 25% までを消すマスクを2つかけて(コードの例とは違い 25% にしました)、左の辺の中ほど(端から 2px)の不透明度を測りました。add では 1 のままで、intersect では 0.05 になりました。intersect は、両方のマスクが重なる所だけを残すので、どちらかのマスクで消した所は消えます。
影やフォーカスの枠が切れる
mask-image を付けた要素では、枠線(border-box)の外側に描かれる影や outline も消えます。検証環境で、box-shadow: 0 0 0 12px #000 の影を付けた箱に、全体を表示するマスク(linear-gradient(#000, #000))をかけると、箱の外の影は表示されませんでした。:focus で付けた outline も消えました。
MDN の mask-clip のページにあるとおり、マスクがかかる範囲は mask-clip で決まり、その外に描いたものは切り取られます。初期値は border-box で、要素の枠線の外側は範囲に入りません。
カードのように影を付けたい要素なら、マスクはカードではなく、中の画像に付けます。
.card {
box-shadow: 0 0 0 12px #000;
}
.card img {
mask-image: linear-gradient(to bottom, #000 50%, transparent);
}
検証環境で、カードの中の img にマスクを付けると、カードの影は消えず、中の画像だけが下に向かって消えました。
mask-clip: no-clip を書くと、影が表示されました。ただし、互換性データでは Safari は no-clip に対応していません。
マスクで透明にした所も、クリックは要素に届きます。検証環境で、下半分を完全に透明にした箱の、下半分の位置をクリックすると、箱の click イベントが起きました。透明にした部分の下にボタンを重ねて置いた場合も、その位置をクリックすると、click が起きたのは箱のほうで、ボタンには届きませんでした。
続きがある側だけを消す
一覧の両端をいつも消していると、いちばん左までスクロールしているときにも、左の端が消えています。スクロール位置に合わせて、続きがある側だけを消すには、スクロール駆動アニメーションで、消す幅を動かします。

@property --fade-left {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@property --fade-right {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
.list {
overflow-x: auto;
--fade-left: 0px;
--fade-right: 0px;
mask-image: linear-gradient(
to right,
transparent,
#000 var(--fade-left),
#000 calc(100% - var(--fade-right)),
transparent
);
}
@supports (animation-timeline: scroll()) {
.list {
animation: fade-edges linear both;
animation-timeline: scroll(self inline);
}
}
@keyframes fade-edges {
0% { --fade-left: 0px; --fade-right: 60px; }
10% { --fade-left: 60px; }
90% { --fade-right: 60px; }
100% { --fade-left: 60px; --fade-right: 0px; }
}
--fade-left と --fade-right は、左右それぞれの消す幅です。@property で <length> の型を付けておくと、アニメーションの途中で値が少しずつ変わります。MDN の scroll() のページにあるとおり、scroll(self inline) は、要素自身の横方向のスクロール位置をアニメーションの進み具合にします。いちばん左が 0%、いちばん右が 100% です。
検証環境で、幅 400px、スクロールできる量が 600px の枠で値を読むと、次のようになりました。
| scrollLeft | –fade-left | –fade-right |
|---|---|---|
| 0 | 0px | 60px |
| 30 | 30px | 60px |
| 300 | 60px | 60px |
| 570 | 60px | 30px |
| 600 | 60px | 0px |
左の端は、スクロールできる量の最初の 10%(この枠では 60px)を進むあいだに、だんだん消えるようになります。右の端は、最後の 10% で、だんだん消えなくなります。
@supports で囲んでいるのは、animation-timeline が効かないときに、スクロール位置が 0 でも左の端が消え、右の端が消えない状態にならないようにするためです。検証環境で、animation-timeline を書かずに同じ animation だけを残すと、アニメーションの長さが 0s になり、スクロール位置が 0 でも、いちばん右まで進んだときの値(左が 60px、右が 0px)のままになりました。MDN の @supports のページにあるとおり、@supports の中は、その機能に対応したブラウザーでだけ使われます。検証環境で @supports の中を消した状態にすると、--fade-left と --fade-right は 0px のままで、左右どちらの端も消えませんでした。
対応しているブラウザー
互換性データでは、接頭辞なしの mask-image に対応しているのは、Chrome 120、Edge 120、Firefox 53、Safari 15.4(iOS 版も 15.4)からです。それより前の Chrome と Safari は、-webkit-mask-image だけに対応しています。検証環境の Chromium 153 では、-webkit-mask-image だけを書いても、同じように下が消えました。古いブラウザーも対象にするなら、両方を書いておきます。
.photo {
-webkit-mask-image: linear-gradient(to bottom, #000 50%, transparent);
mask-image: linear-gradient(to bottom, #000 50%, transparent);
}
mask-composite は Chrome 120、Edge 120、Firefox 53、Safari 15.4 から、animation-timeline の scroll() は Chrome 115、Edge 115、Firefox 159、Safari 26 から対応しています。





