マネーフォワードクラウドの見た目を変えるChrome拡張を作った ― invertフィルターとCSS詳細度ハックでダークモードを力技で作る
マネーフォワードクラウドには、公式のダークモードがありません。夜間に作業していると画面の白さが目に負担になるので、CSSを差し込んで無理やりダークテーマ化するChrome拡張「マネーフォワード クラウド カスタムテーマ」を作りました。あわせて、アクセントカラーの変更、フォントの変更、不要な要素を選んで非表示にする機能も入れています。
自分専用の拡張で、対象はマネーフォワードクラウド(*.moneyforward.com)のみです。freeeなど他の会計ソフトでは動作しません。Chromeウェブストアにも公開しておらず、デベロッパーモードで読み込んで使っています。
きっかけになったのは、マネーフォワードでForward Deployed Engineerを務める方が書いた、日付選択のショートカットや源泉所得税の自動計算などをMFクラウド会計に足すChrome拡張「MFCA Labs」の開発記録です(SaaS企業の現場に入り込むFDEが、経理の「あったらいいな」をChrome拡張機能でつくった話)。権限をMFクラウドのドメインだけに絞り、外部サーバーへは一切データを送らないという設計方針は、この記事から踏襲しています。
作ったもの
Manifest V3の拡張で、content.jsとstyles.cssだけの小さな構成です。権限はstorageのみで、外部通信は行いません。
| 機能 | 内容 |
|---|---|
| ダークモード | ページ全体をワンクリックで反転表示 |
| アクセントカラー変更 | リンク・ボタンの色をカラーピッカーで指定 |
| フォント変更 | 游ゴシックやメイリオに加え、手書き風フォントも選択可 |
| 要素の非表示 | 画面上の要素をクリックして選び、そのまま非表示に |
右下にフローティングパネルを置き、そこから各設定を切り替える作りです。設定はchrome.storage.localに保存され、次回アクセス時に自動で復元されます。
山場1:ダークモードは「配色を作る」のではなく「反転させる」
まともなダークテーマを作るなら、背景色・文字色・境界線色などをひとつずつ定義していく必要があります。ですが、他社が作った複雑な画面のCSS変数を全て洗い出すのは現実的ではないので、今回はfilter: invert(1) hue-rotate(180deg)をhtml要素にかけて、画面全体の色を丸ごと反転させる力技を選びました。
html.mfx-dark {
filter: invert(1) hue-rotate(180deg);
background: #fff;
}
html.mfx-dark img,
html.mfx-dark video,
html.mfx-dark svg,
html.mfx-dark canvas {
filter: invert(1) hue-rotate(180deg);
}
このままだと画像やアイコンSVGまで反転して色が変になるので、img・video・svg・canvasに対してもう一度同じフィルターをかけて打ち消しています(反転を2回かけると元に戻る)。
さらに、自分が作った操作パネル自体もhtml.mfx-darkの子要素なので反転の対象に巻き込まれてしまいます。パネルの見た目まで反転して見づらくなるのは本末転倒なので、パネルにも同じフィルターを重ねて元の見た目に戻しています。
html.mfx-dark #mfx-panel {
filter: invert(1) hue-rotate(180deg);
}
「反転を打ち消すためにもう一度反転をかける」という発想は、力技のわりに応用が利きます。ダークモード非対応のSaaSに後から色を差し込みたいときの、手数の少ない選択肢のひとつだと思います。
山場2:!important同士のCSS詳細度勝負
アクセントカラーやフォントを変更するにあたって、マネーフォワード側のコンポーネントがfont-familyなどに!importantを付けて指定しているケースがありました。こちらも!importantを付ければ上書きできるかというと、そう単純ではありません。両方に!importantが付いている場合、最終的にどちらが勝つかは通常のCSS詳細度(セレクタの強さ)の勝負になります。
そこで、実在しないID #mfx-force への:not()を何個も連ねて、セレクタの詳細度を人為的に吊り上げるという方法を取りました。
const SPECIFICITY_BOOST =
":not(#mfx-force):not(#mfx-force):not(#mfx-force):not(#mfx-force)";
css += `
body${SPECIFICITY_BOOST}, body *${SPECIFICITY_BOOST} {
font-family: ${settings.fontFamily} !important;
}
`;
:not()は中身が実在しないIDであっても、擬似クラスとして詳細度の計算には加算されます。存在しない条件を否定形で書くことで、見た目のスタイルには一切影響を与えずに詳細度だけを底上げできる、というCSSの仕様の隙を突いたやり方です。他社の管理下にあるSaaSの画面に自分のスタイルを確実に勝たせたいとき、正攻法で詳細度を積むよりも見通しが良いと感じました。
山場3:フォント変更でアイコンまで巻き込んで「豆腐化」した
フォント変更をbody *全体にかけたところ、矢印やチェックマークなどのアイコンフォントまで指定したフォントに書き換わってしまい、意図しない四角い文字(いわゆる「豆腐」)が並ぶ事態になりました。アイコンフォントは、実際の文字コードに対応する専用フォントのグリフをアイコンとして表示する仕組みなので、フォントを丸ごと差し替えると表示が崩れます。
対処としては、class名にicon・Icon・fa-・glyph・arrow・chevronといった文字列を含む要素と、svg・use・iタグを対象から除外し、font-family: revertでブラウザ標準の挙動に戻しています。
body i:not(#mfx-force)...,
body svg:not(#mfx-force)...,
body [class*="icon" i]:not(#mfx-force)...,
body [class*="fa-"]:not(#mfx-force)... {
font-family: revert !important;
}
厳密な仕組みで判定しているわけではなく、命名規則をキーワードで拾っているだけのアドホックな対策です。それでも実際に使ってみて崩れが出ていないので、他社の画面という中身を完全には把握できない対象に対しては、こういう「よくある命名パターンで弾く」割り切りが現実的だと感じました。
山場4:要素ピッカーはCSSセレクタパスを都度生成する
「不要な要素を選んで非表示にする」機能では、クリックした要素からCSSセレクタのパスを組み立ててchrome.storage.localに保存し、次回アクセス時にそのセレクタへdisplay: noneを当てて再現しています。
function cssPath(el) {
const path = [];
while (el && el.nodeType === Node.ELEMENT_NODE && el !== document.body) {
let selector = el.nodeName.toLowerCase();
if (el.id) {
selector += "#" + CSS.escape(el.id);
path.unshift(selector);
break;
} else {
let sibling = el, nth = 1;
while ((sibling = sibling.previousElementSibling)) {
if (sibling.nodeName.toLowerCase() === el.nodeName.toLowerCase()) nth++;
}
selector += `:nth-of-type(${nth})`;
}
path.unshift(selector);
el = el.parentElement;
}
return path.join(" > ");
}
idがあればそこで打ち切り、なければnth-of-typeで兄弟要素の中の何番目かを数えながら親へ遡っていく、素朴な実装です。マウスオーバーで対象要素をハイライトし、クリックで確定するUIにしたことで、DOM構造やCSSクラス名を自分で調べなくても「見た目で選んで消す」操作感になりました。
設計上の判断
- 配色を作り込まずフィルターで反転させた理由:他社のSaaSの画面デザインを完全に把握することはできないので、個別に色を定義するアプローチは保守が破綻しやすいと判断しました。反転方式は多少見た目の質は落ちますが、画面のアップデートに強い実装になります。
- freeeを対象にしなかった理由:普段の実務でマネーフォワードクラウドを使う時間が長く、まず自分が困っている画面から着手しました。freee向けに同じ仕組みを作れるかは未検証です。
セレクタ依存の保守ポイント
要素ピッカーで保存したセレクタは、nth-of-typeという相対的な位置情報に依存しています。マネーフォワード側の画面レイアウトが変わって要素の並び順が変わると、意図しない要素が消えたり、消したい要素が消えなくなったりします。壊れたら都度セレクタを選び直す前提の割り切りです。
まとめ
公式にダークモードがないSaaSに後付けで色を差し込む場合、個別のスタイルを作り込むより、filter: invert()のような画面全体にかける力技のほうが保守コストは低く済みます。ただし、その分だけ画像やアイコン、自分のUIパネルなど「反転させたくないもの」を都度打ち消す手当てが必要になる、というのが実装してみての実感です。
この内容について相談したい場合は
無料相談を申し込む