この記事の概要
2023 年 9 月 8 日に、Qiita ではダークテーマ UI の提供を開始しました。
Qiita が生まれたのは 2011 年 9 月 16 日で、およそ 12 年の月日を経てダークテーマへ対応しました。
これだけの期間続いているサービスにダークテーマを適用した事例は日本でも少ないと思うので、実施したことを記事にしてみました。
なお、Qiita 特有のコードの話は少なめに、多くの場所で役立ちそうな内容に絞っています。
対象読者
- UI デザイナー / フロントエンドエンジニア
- 現在運用中のサービスにダークテーマを適用したいと考えている人
- ダークテーマ単体の作り方は分かるけど、既存の UI に上手く追加する方法が分からない人
実施したこと
- デザイントークンの更新
- モックアップにトークンを適用
- ベータ版としての提供
- 正式版としての提供
デザイントークンの更新
まずはデザイントークンの更新から始めました。
これまでの Qiita にもデザイントークンは存在していましたが、グローバルトークンが大半で、エイリアストークンは少ししかありませんでした。
つまり、以下のような内容です。
:root {
/* グレーや緑など、カラーパレットはあくまでグローバルトークン的な命名 */
--color-gray-0: #fff;
--color-gray-5: #f9fafa;
--color-gray-10: #f5f6f6;
--color-gray-20: #edeeee;
--color-gray-30: #dfe0e0;
--color-gray-40: #bcbdbd;
--color-gray-50: #9c9e9e;
--color-gray-60: #7a7d7d;
--color-gray-70: #5e6060;
--color-gray-80: #494b4b;
--color-gray-90: #3a3c3c;
--color-gray-100: #2f3232;
/* ... */
/* 文字色はもともとエイリアストークン的な命名 */
--color-text-disabled: rgb(0 0 0 / 38%);
--color-text-medium-emphasis: rgb(0 0 0 / 60%);
--color-text-high-emphasis: rgb(0 0 0 / 87%);
}
例えば以下のような 薄いグレーの背景に白いカードが載っている
ページがある場合の実装方法を見てみます。
かつての CSS はこのようになっていました(色に関わるプロパティだけを抜き出したイメージです)。
.main {
background-color: var(--color-gray-10);
color: var(--color-text-high-emphasis);
}
.article {
background-color: var(--color-gray-0);
}
現状のトークンだけでダークテーマに対応しようと思うと、以下のようなコードになりかねません。
.main {
background-color: var(--color-gray-10);
color: var(--color-text-high-emphasis);
}
.article {
background-color: var(--color-gray-0);
}
+ @media (prefers-color-scheme: dark) {
+ .main {
+ background-color: var(--color-gray-110);
+ color: rgb(255 255 255 / 87%);
+ }
+
+ .article {
+ background-color: var(--color-gray-100);
+ }
+ }
この実装ではいくつかの問題点があります。
- ほぼすべての要素に
@media (prefers-color-scheme: dark)
を指定しなければならない - エイリアストークン風の命名の CSS Custom Property にライトテーマ用の値しか無いため、ダークテーマ用のスタイルではハードコーディングが必要
これらを解消するために、グローバルトークンとエイリアストークンを明確に分けました。
以下はおおよそのイメージです。
:root {
/* color global */
--color-gray-0: #fff;
--color-gray-5: #f9fafa;
--color-gray-10: #f5f6f6;
--color-gray-20: #edeeee;
--color-gray-30: #dfe0e0;
--color-gray-40: #bcbdbd;
--color-gray-50: #9c9e9e;
--color-gray-60: #7a7d7d;
--color-gray-70: #5e6060;
--color-gray-80: #494b4b;
--color-gray-90: #3a3c3c;
--color-gray-100: #2f3232;
/* ... */
/* color alias */
+ --color-background: var(--color-gray-10);
+ --color-surface: var(--color-gray-0);
+ --color-surface-variant: var(--color-gray-20);
--color-text-disabled: rgb(0 0 0 / 38%);
--color-text-medium-emphasis: rgb(0 0 0 / 60%);
--color-text-high-emphasis: rgb(0 0 0 / 87%);
}
+ @media (prefers-color-scheme: dark) {
+ :root{
+ --color-background: var(--color-gray-110);
+ --color-surface: var(--color-gray-100);
+ --color-surface-variant: var(--color-gray-80);
+ --color-text-disabled: rgb(255 255 255 / 38%);
+ --color-text-medium-emphasis: rgb(255 255 255 / 60%);
+ --color-text-high-emphasis: rgb(255 255 255 / 87%);
+ }
+ }
このように登録し直すことにより、先ほど挙げた 2 つの問題点を解消できます。
もともとのコードが以下のように変更されることが予想されます。
.main {
- background-color: var(--color-gray-10);
+ background-color: var(--color-background);
color: var(--color-text-high-emphasis);
}
.article {
- background-color: var(--color-gray-0);
+ background-color: var(--color-surface);
}
ちなみに色の命名や数は Material Design を参考にしました。1
モックアップにトークンを適用
そして先ほど更新したデザイントークンをモックアップにも適用してみます。
いくらコードの完成形を意識しながらトークンを更新しているとは言え、いきなり大幅な変更を加えて表示崩れが起きたら大変です。
例のため大袈裟に表現しますが、次のようにページによって背景色が違ったとします。
ページ 1 | ページ 2 |
---|---|
モックアップデータを優先しようとすると、先ほど作った --color-background
という色だけでは足りなくなってしまいます。
しかし、やたらめったら例外を作ると結局大変です。
そのためある程度割り切って、定義したトークンだけで実装できるようにモックアップに手を入れることにしました。
先ほど例に出した 2 つのページで言えば、完全に背景色を揃えてしまいます。
ページ 1 | ページ 2 |
---|---|
これにより、統一されたルールでダークテーマへ変更できるようになりました。
ページ 1 | ページ 2 |
---|---|
デザイナーとしては「ここは微妙に濃くすると見栄えが良い」「少しだけ薄くして要素を引かせたい」などの場面はいくらでもあります。
ただ、そういった調整がすべて叶うトークン作成をしようとすると、数が膨大になります。
数が増えれば増えるほど名前や役割が不明瞭になり、結果的に表示崩れを誘発しかねません。
100 点を取るためのハードコーディングではなく、いつでも 80 点を切らないシステマチックなビジュアルのルールを意識しました。
ベータ版としての提供
これで実装できるのですが、いきなりすべてのユーザーに提供するのは恐ろしいです。
そのためベータ版としての提供を実施し、表示崩れを修正した上で正式版としてリリースしました。
ベータ版時点での挙動は次のようなものでした。
- ベータユーザー
- OS のダークテーマ、ライトテーマにあわせて Qiita のテーマも変更される
- 通常ユーザー
- ライトテーマのみ
これを実現するため、まずは CSS Custom Properties を登録しているファイルを分割しました。
:root {
/* color global */
--color-gray-0: #fff;
--color-gray-5: #f9fafa;
--color-gray-10: #f5f6f6;
--color-gray-20: #edeeee;
--color-gray-30: #dfe0e0;
--color-gray-40: #bcbdbd;
--color-gray-50: #9c9e9e;
--color-gray-60: #7a7d7d;
--color-gray-70: #5e6060;
--color-gray-80: #494b4b;
--color-gray-90: #3a3c3c;
--color-gray-100: #2f3232;
/* ... */
/* color alias */
--color-background: var(--color-gray-10);
--color-surface: var(--color-gray-0);
--color-surface-variant: var(--color-gray-20);
--color-text-disabled: rgb(0 0 0 / 38%);
--color-text-medium-emphasis: rgb(0 0 0 / 60%);
--color-text-high-emphasis: rgb(0 0 0 / 87%);
}
@media (prefers-color-scheme: dark) {
:root{
--color-background: var(--color-gray-110);
--color-surface: var(--color-gray-100);
--color-surface-variant: var(--color-gray-80);
--color-text-disabled: rgb(255 255 255 / 38%);
--color-text-medium-emphasis: rgb(255 255 255 / 60%);
--color-text-high-emphasis: rgb(255 255 255 / 87%);
}
}
そして、ベータユーザーかどうかにあわせて読み込む CSS を変更しました。
Qiita では Rails (slim) を使っているので次のようなコードでした。
= stylesheet_link_tag 'path/to/light.css', media: 'all'
- if beta_release_enabled?
= stylesheet_link_tag 'path/to/dark.css', media: 'all'
これにより、各ページでの CSS には var(--color-background)
と書くだけで、ベータユーザーかどうかにあわせて適切なトークンが読まれ、テーマが分岐します。
裏話
Qiita では Rails の上で React が動いています。
最初はベータユーザーかどうかによる分岐を React 側で行なっていました。
具体的には Emotion の Global Styles の仕組みを使い、ダークテーマを適用したいページにだけ ダークテーマを読み込んだ Global コンポーネントを当てる
ようなやり方です。
分岐自体は上手くできていたのですが、レンダリングタイミングの問題もあり、読み込み後一瞬ライトテーマの UI が表示されてしまっていました。
「せっかくダークテーマを試しているのに、これじゃかえって目がチカチカする」という声を多くいただき、後日チラつきが起きないように Rails 側での分岐に変えました。
正式版としての提供
ベータ版での検証や修正も完了し、いよいよ正式版です。
ベータ版では OS のテーマ設定にあわせて Qiita のテーマも自動で変わるようにしていましたが「連動して欲しく無い」という声もいただきました。
そのため、ヘッダーのメニューからテーマを変更できるようにした上で正式版として提供しました。
なお、この記事を投稿している 2023 年 10 月 6 日現在はすべてのユーザーのみなさんにダークモードをご利用いただけます。
ぜひログインして設定を有効にしてみてください。
最後に
ベータ版では黒い背景なのに文字色が黒いままのページなど、まったく読めないページを提供してしまったこともありました。
ユーザーのみなさんのご協力によって解消できたページが多く存在します。
また、実装も私ひとりで行ったわけがなく、社内の多くのエンジニア・デザイナーの協力を仰ぎました。
この場を借りてお礼申し上げます。
-
私が参考にしていた時期から更に Material がアップデートされてしまい、引用した画像と例示したコードに微妙に食い違いがあります。ご了承ください。 ↩