CSSで画像を2色のデュオトーンにする方法
デュオトーンは、画像の暗い部分を1つの色、明るい部分をもう1つの色にして、その間の明るさを2色のあいだの色でつなぐ表現です。CSS では、SVG のフィルターを filter: url() で当てる方法と、mix-blend-mode で色を重ねる方法の2つで作れます。指定した2色のとおりに仕上がるのは SVG のフィルターのほうで、mix-blend-mode では明るい側の色が少しずれました。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。上に色のある景色、下に黒から白への階調を置いた 256×192 ピクセルの画像を作り、2色にしたものを Playwright で撮って、画素の色を数値で調べました。Firefox と Safari、スマートフォンの実機は試していません。ほかのブラウザーの対応状況は、MDN のブラウザー互換性データ(@mdn/browser-compat-data 8.1.8)の値です。
ここでは、暗い部分を #1f2a30(黒に近い紺)、明るい部分を #00c8e8(水色)にします。

SVG のフィルターで、指定した2色にする
HTML の中に、見えない <svg> を置いてフィルターを定義し、CSS の filter で画像に当てます。
<svg width="0" height="0" style="position: absolute" aria-hidden="true">
<filter id="duotone" color-interpolation-filters="sRGB">
<!-- まず白黒にする -->
<feColorMatrix type="saturate" values="0" />
<!-- 黒を #1f2a30、白を #00c8e8 に置きかえ、間をつなぐ -->
<feComponentTransfer>
<feFuncR type="table" tableValues="0.122 0" />
<feFuncG type="table" tableValues="0.165 0.784" />
<feFuncB type="table" tableValues="0.188 0.910" />
</feComponentTransfer>
</filter>
</svg>
<img class="duotone-svg" src="photo.png" width="256" height="192" alt="">
.duotone-svg { filter: url(#duotone); }
フィルターは2段階で色を変えます。
1段目の <feColorMatrix> は、type="saturate" で values="0" にすると、色の鮮やかさを0にして白黒にします。Filter Effects の仕様では、このときの明るさは赤・緑・青に 0.2126、0.7152、0.0722 を掛けて足した値です。CSS の grayscale(1) も同じ係数です。
2段目の <feComponentTransfer> は、赤・緑・青のそれぞれを、子の要素 <feFuncR>・<feFuncG>・<feFuncB> で決めた値に置きかえます。type="table" の tableValues に2つの数を書くと、0(黒)を1つめの数に、1(白)を2つめの数にして、その間を直線でつなぎます。白黒にした後なので、どの画素も「暗い色から明るい色までの線の上のどこか」の色になります。
tableValues には、色の値を 0〜1 で書きます。16進数の色を 255 で割った値です。
| 赤 | 緑 | 青 | |
|---|---|---|---|
暗い部分 #1f2a30 |
31 ÷ 255 = 0.122 | 42 ÷ 255 = 0.165 | 48 ÷ 255 = 0.188 |
明るい部分 #00c8e8 |
0 ÷ 255 = 0 | 200 ÷ 255 = 0.784 | 232 ÷ 255 = 0.910 |
検証環境で、下の階調の帯を測ったところ、黒だった左端は (31, 42, 47)、白だった右端は (0, 199, 232) になりました。#1f2a30 は (31, 42, 48)、#00c8e8 は (0, 200, 232) なので、どちらも1以内の差です。景色の部分も含めて全部の画素を、「白黒にした明るさで2色の間をつないだ色」と比べると、差はいちばん大きいところで 1.33 でした。
検証環境の Chromium では、<svg> を style="display: none" で隠しても、フィルターは同じ色で効きました。
color-interpolation-filters="sRGB" を忘れると色がずれる
上のコードの <filter> には、color-interpolation-filters="sRGB" を書いています。これを書かないと、フィルターは指定した色になりません。
color-interpolation-filters は、フィルターがどの色空間で計算するかを決める属性で、初期値は linearRGB です。linearRGB では、明るさを光の量に比例する値に直してから計算します。tableValues の数もその値として扱われるので、#1f2a30 を 255 で割った値を書いても、画面に出る色は #1f2a30 になりません。CSS の grayscale() のようなフィルター関数は、この属性に関係なく sRGB で計算すると、仕様に書かれています。

検証環境で、color-interpolation-filters だけを消したフィルターを当てると、階調の帯の色は次のようになりました。
| 帯の位置 | sRGB を指定 | 指定なし(linearRGB) |
|---|---|---|
| 左端(元は黒) | (31, 42, 47) | (98, 113, 119) |
| まん中(元は 128 の灰色) | (15, 121, 140) | (86, 148, 158) |
| 右端(元は白) | (0, 199, 232) | (0, 229, 245) |
指定しないと、黒が灰色になり、画像全体が白っぽくなりました。デュオトーンのフィルターでは、color-interpolation-filters="sRGB" を <filter> に書いておきます。
mix-blend-mode で、CSS だけで2色にする
HTML に <svg> を置かずに、CSS だけで作る方法もあります。画像を囲む要素の背景を明るい色にし、その上に白黒にした画像と、暗い色で塗った層(::after)を、mix-blend-mode で順に重ねます。
<div class="duotone-blend">
<img src="photo.png" width="256" height="192" alt="">
</div>
.duotone-blend {
position: relative;
background: #00c8e8; /* 明るい部分の色 */
}
.duotone-blend img {
filter: grayscale(1);
mix-blend-mode: multiply;
}
.duotone-blend::after {
content: "";
position: absolute;
inset: 0;
background: #1f2a30; /* 暗い部分の色 */
mix-blend-mode: screen;
}
Compositing and Blending の仕様では、色の値を 0〜1 としたとき、multiply は下の色と上の色を掛けた値、screen は「下 + 上 − 下 × 上」です。白黒にした画像を水色の背景に multiply で重ねると、黒は黒のまま、白は水色になります。その上に紺の層を screen で重ねると、黒だったところは紺になります。
検証環境で測ると、階調の帯の左端(元は黒)は (31, 42, 48) で、#1f2a30 と同じでした。ところが右端(元は白)は (31, 209, 236) で、#00c8e8 の (0, 200, 232) より明るくなりました。紺の層は明るい部分にも screen で重なるので、白だったところは、水色と紺を screen で重ねた色になります。この例では明るい色の赤が 0 で、紺の赤は 31 なので、階調の帯で測った5か所とも、赤は 31 でした。全部の画素を、multiply と screen の式で計算した色と比べると、差はいちばん大きいところで 1.21 で、式どおりに重なっていました。
暗い側の色は指定どおりになりますが、明るい側の色は、2色を screen で重ねた色になります。明るい色を指定どおりに出したいときは、SVG のフィルターを使います。
どちらを使うか
2つの方法の違いをまとめます。
| SVG のフィルター | mix-blend-mode | |
|---|---|---|
| 黒だったところの色 | 指定した暗い色 | 指定した暗い色 |
| 白だったところの色 | 指定した明るい色 | 2色を screen で重ねた色 |
| 書く場所 | HTML に <svg>、CSS に filter |
CSS だけ(HTML には画像を囲む要素を書く) |
| 色の書き方 | 0〜1 の数に直して書く | CSS の色のまま書ける |
MDN のブラウザー互換性データでは、CSS の filter プロパティ自体は Chrome 53、Edge 12、Firefox 35、Safari 9.1 から、mix-blend-mode は Chrome 41、Edge 79、Firefox 32、Safari 8 から対応しています。<feColorMatrix> と <feComponentTransfer> は、Chrome 5、Edge 12、Firefox 3、Safari 6 から対応しています。





