God said

ダークモードに対応しました

dark-mode-topParody of WWDC 2019 Keynote - Introducing iOS 13 in Dark Mode | Apple (YouTube)

騒がしい日々に、いかがお過ごしでしょうか。
YOASOBIの『夜に駆ける』をヘビロテしつつ書いています。本記事はそんな夜での閲覧が捗りそうなテーマを扱います。

2018年発表のmacOS Mojave、2019年発表のiOS 13でダークモードがサポートされ、メジャーなアプリやウェブサイトは続々とダークテーマを追加してきています。ちょっと興味があったので、白を基調としたこのサイトも勉強がてらダークモードに対応させてみることにしました。

切り替え


切り替えはウィジェット最上部のトグル、もしくはスクロール時に表示される固定ヘッダー右端のトグルから行えます。切り替わりの仕組みはごく単純で、<body>タグに.darkを追加することで、あらかじめ用意しておいたダークテーマ用のスタイルを上書きで有効にさせる原始的なものです。

ブラウザのCookieにどちらのテーマを使っているか保存していますので、しばらくの間は再訪問時でも前回利用したテーマが自動で適用されます。

電灯のスイッチのようにバチッと切り替わるのは無骨な感じがしたので、CSSアニメーションでシャッターの開閉を入れて切り替えの瞬間を隠すようにしています。端末によって若干重かったりしますが楽しいので採用。

God said
裏で切り替えている間はシャッターが閉まったままなので、聖書の有名な一節を引用して表示しています。地球に昼と夜をもたらした神は、ライトテーマ・ダークテーマの祖と言えるでしょう。多分。

トグルのデザインはCodePenに投稿されていた「Dark/Light Toggle」をお借りし、一部簡略化しています。素晴らしいデザインに感謝。

See the Pen
Dark/Light Toggle
by Rogie (@rogie)
on CodePen.

カラーセット

:root {
    --base-bg: #161716; /* 全体背景 */
    --main-bg: #202020; /* 本文背景 */
    --sub-bg: #2d2d2d; /* blockquoteなどの背景 */
    --main-text: #d7d7d8; /* 本文用テキスト色 */
    --sub-text: #bbb; /* ウィジェット用テキスト色 */
    --link: #1abb9a; /* リンク用テキスト色(両モード共通) */
    --border: #3f3f3f; /* ボーダー */
}

自分で考えても碌なことにならないので、WebKitのウェブサイトに採用されている配色を大いに参考にしました。

背面に行くほど暗く、前面に行くほど明るくするのが自然なので、背景もテキストもそれに沿って色を変えています。

ちなみにダークだからといってピュアブラック(#000000)を背景に据えるのはNG。高コントラストによる目の負担を避けるためと、最近iPhoneなどに採用され始めたOLEDディスプレイ特有の表示の歪み(Black Smearing)を起こさないようにするためというのが主な理由です。同様にピュアホワイト(#ffffff)も避けられることが多いとか。

画像への対応

写真や図などに関しては基本的にfilter: brightness(.85);で明るさを落とすことで対応しました。サイトロゴや先ほどの聖書の一節のような黒一色の透過png/svgであれば、filter: invert(1);で色を反転させて表示しています。filterプロパティほんと便利。

以上で対応できない場合のみ、ダークテーマ用に新しく画像を用意して切り替える方法を取りました。

埋め込みツイートへの対応

ダークモード対応にあたって最もこだわったところです。ダークモードに切り替えると記事内の埋め込みツイート、及びウィジェット下部の埋め込みタイムラインもダークテーマに変わります。
ダークテーマを導入したサイトは数多く拝見しましたが、ここまでやっているところはありませんでした。シンプルに面倒ですし、Twitterの仕様変更の煽りをストレートに受けるので誰もやりたがらないだけだとは思うのですが、実際にダークテーマで閲覧中に突然真っ白なツイートが飛び込んでくると結構目に来るので、対処しておく価値はあると思います。

サンプルのツイートを置いておきます。切り替えて確認してみてください。PC表示の方はちょうど右側の埋め込みタイムラインも一緒に確認していただけると思います。

埋め込みコードから生成される埋め込みツイートは、おおまかに以下のような構造になっています(2020年6月時点)。

<twitter-widget>
  └─ #shadow-root (open)
       ├─ .SandboxRoot
       │    └─ .EmbeddedTweet /* 背景色指定など */
       │         └─ .EmbeddedTweet-tweetContainer
       │               ├─ .EmbeddedTweet-tweet
       │               │    └─ <blockquote>
       │               │         ├─ .Tweet-header /* プロフィールアイコンやスクリーンネームなど */
       │               │         │    └─ ...
       │               │         └─ .Tweet-body /* ツイート本文や投稿日時など */
       │               │              └─ ...
       │               │
       │               └─ .CallToAction /* 「〇〇人がこの話題について話しています」の部分 */
       └─ <style>
            └─ @import url("https:// ... .light.ltr.css"); /* ウィジェット用のCSSファイル */

ポイントは一番下。ウィジェット用のスタイルシートが読み込まれていますが、URLが示すようにライトテーマ用とダークテーマ用に分けて用意されているようです。実際にDeveloper Toolsでlightdarkに書き換えてあげるだけで、即座にダークテーマに変わります。
よってツイートが読み込まれた後に、状況に応じてこの部分を動的に差し替えるような仕組みを作ればよさそうです。

埋め込みツイートはShadow DOMで分離されていますが、ありがたいことにopenなのでelement.shadowRootでアクセスできます。

function changeEmbeddedTweetCss() {
    var selectWidgets = document.querySelectorAll('twitter-widget');
    var shadowRootStyle;
    selectWidgets.forEach( tw => {
        shadowRootStyle = tw.shadowRoot.querySelector('style:last-child');
        if ( cookieText == 'Active' ) {
            shadowRootStyle.innerHTML = shadowRootStyle.innerHTML.replace( 'light.ltr.css', 'dark.ltr.css' );
        } else {
            shadowRootStyle.innerHTML = shadowRootStyle.innerHTML.replace( 'dark.ltr.css', 'light.ltr.css' );
        }
    });
}

しかしその切り替え先となるダークテーマに問題がひとつ。
twttr-default-theme
背景にあの憎きピュアブラック(#000000)がべったりと。これはどうにかしたいところです。埋め込みタイムラインは初めからダークグレイ(#292f33)指定なのになぜこちらはこうなのか。

幸いにもlightdarkの切り替え場所が<style>タグ内なので、ついでにピュアブラックを上書きで修正する指定も差し込んであげたいと思います。

そしてついでのついでに、かねてから要らないと思っていた「〇〇人がこの話題について話しています (... people are talking about this)」の部分も削ぎ落とします。最終的に<style>タグ内に差し込むスタイルは以下のようになりました。

.CallToAction {
    display: none;
}
.EmbeddedTweet {
    background-color: #2d2d2d;
}

これをよしなにすると以下のようになります。コードも恥も晒していけのスタンスなので、おかしなところがあれば優しく教えて下さい。

var toggleCount = 0;
function addEmbeddedTweetCss() {
    var selectWidgets = document.querySelectorAll('twitter-widget');
    var shadowRootStyle;
    var insertHTML = '.CallToAction {display: none;}';
    if ( toggleCount == 0 ) { /* 画面遷移時 */
        if ( cookieText == 'Active' ) {
            insertHTML += '.EmbeddedTweet {background-color: #2d2d2d;}';
        } else {
            insertHTML += '.EmbeddedTweet {background-color: #ffffff;}';
        }
        selectWidgets.forEach( tw => {
            shadowRootStyle = tw.shadowRoot.querySelector('style:last-child');
            shadowRootStyle = shadowRootStyle.insertAdjacentHTML( 'beforeend', insertHTML );
        });
    } else { /* トグルクリック時 */
        if ( cookieText == 'Active' ) {
            selectWidgets.forEach( tw => {
                shadowRootStyle = tw.shadowRoot.querySelector('style:last-child');
                shadowRootStyle.innerHTML = shadowRootStyle.innerHTML.replace( '#ffffff', '#2d2d2d' );
            });
        } else {
            selectWidgets.forEach( tw => {
                shadowRootStyle = tw.shadowRoot.querySelector('style:last-child');
                shadowRootStyle.innerHTML = shadowRootStyle.innerHTML.replace( '#2d2d2d', '#ffffff' );
            });
        }
    }
    toggleCount += 1;
}

これで晴れて思い通りの表示になりました。

他に埋め込みタイムラインやEmbedlyもダークモードに対応させましたが、これらはiframeによって埋め込まれているためアプローチが少し異なります。このあたりの話は執筆中のもう1本の記事にお任せしたいと思います。


【2020年6月24日 追記】
閲覧環境によってはShadow DOMではなくiframeで埋め込まれる場合があるようです。取り急ぎどちらでもダークテーマに変わるよう修正しました。
ただしiframeはセキュリティの関係上、背景色を変更したり一部を非表示にしたりといったカスタマイズはできません。