DocBaseのダークテーマ対応 〜デザイン・フロントエンド編〜

クレイの asachun です。
ついに DocBase も[ダークテーマ](https://kray.jp/blog/dark-theme-frontend/#:~:text=%E3%81%A4%E3%81%84%E3%81%AB%20DocBase%20%E3%82%82-,%E3%83%80%E3%83%BC%E3%82%AF%E3%83%86%E3%83%BC%E3%83%9E%E3%81%AB%E5%AF%BE%E5%BF%9C,-%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%EF%BC%81%EF%BC%88%E9%95%B7%E3%82%89%E3%81%8F)に対応しました!(長らくお待たせしました……!)
今回はデザイン・フロントエンドの観点から、どのようにダークテーマ対応を進めていったかご紹介します。
## フロントエンド改修時にダークテーマ対応の話は出ていた
実は[フロントエンド改修](https://kray.jp/blog/how-to-docbase-renewal/)の段階でダークテーマ対応を検討し始めていました。ですが、フロントエンド改修が大きな作業のため、同時に進めることは避けていたのです。
ただフロントエンド改修の際に、**ダークテーマ対応を見据えた仕組みも作っていただいていました。**
他の作業の合間に少しずつダークテーマ対応を進め、この度リリースとなりました :tada:
## デザインの段階でライト⇔ダークのカラーを定義
フロントエンド改修の際にデザインデータも Figma で作り直したのですが、そのときに汎用的なカラーはダークテーマ時のものも定義していました。

Figma でのダークテーマ制作の際には [Dark mode swicther](https://www.figma.com/community/plugin/808916768872750785/dark-mode-switcher) プラグインに大変お世話になりました!
Color styles を定義するときに、`Light/bg/base`のように`Light|Dark`を含めたスラッシュ区切りの名前にすると、プラグイン実行時に一発でダーク/ライトカラーを変換してくれます。
コンポーネント単位で実行すれば、`Light`や`Dark`の含まれる複数の Cololr styels を一度に変換できるので大変重宝しました。
(ときどき変換に失敗するときもあるのですが、1つ1つ切り替えるより断然楽です!)
あと Figma はコンポーネントを選択したら、子孫コンポーネントで使われてる色をすべて表示してくれるのがいいですよね。色の変更も簡単にできますし、この機能を考えた人は神だと思います。

## テーマ切り替えの実装
テーマ切り替えの仕組みはパートナーのフロントエンドエンジニアの方に実装していただいたので、ブログ用の文章もご寄稿いただきました :bow: ありがとうございます!
(こちらのセクションはご寄稿いただいたものになります)
DocBase でのテーマ切り替えの実装には、[styled-components](https://styled-components.com/) がビルトイン機能として提供している [ThemeProvider コンポーネント](https://styled-components.com/docs/advanced#theming)を使用しています。
フロントエンド改修のための技術選定の際(2020年頃)に、[Sass](https://sass-lang.com/) から styled-components への移行を決定し、そのときにテーマ切り替えの仕組みも `ThemeProvider` を使って導入しました。
この `ThemeProvider` を使うと、前述のような汎用カラーの定義を元に、色の値だけが異なる同じ型のオブジェクトをテーマごとに用意することで(下記 `lightTheme` と `darkTheme` )、そのオブジェクトを切り替えるだけで簡単にテーマ管理ができるようになります。
```tsx
const lightTheme = { /* ライトテーマの色指定 */ }
const darkTheme = { /* ダークテーマの色指定 */ }
const App = () => {
return (
{ /* ThemeProvider配下のコンポーネントがテーマ対応される */ }
)
}
```
### コンポーネント固有のテーマ別の色指定
前述の `ThemeProvider` を使った色(テーマ)の管理は、汎用カラーでどのコンポーネントからでも参照できるスコープになるので、いわばグローバル変数のような扱いになります。
ただ、実際のコンポーネント作成では、**グローバル変数にする必要のないコンポーネント固有の色指定も存在し、その場合はグローバルスコープを避けてローカル変数のような扱いにしたいです**。
そのような「色指定をコンポーネント内に閉じ込めたい」ケースでは、styled-components が提供している [styled-theming](https://kray.jp/blog/dark-theme-frontend/#:~:text=%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B-,styled%2Dtheming,-%E3%81%A8%E3%81%84%E3%81%86%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%82%92) というライブラリをベースに、DocBase 独自にカスタマイズした `theming` 関数を用意して対応しました。
```tsx
const BarComponent = styled.div(() => {
// 独自関数themingを使って、コンポーネント内に閉じたテーマ別(light/dark)の色指定を行う
const colors = {
text: theming({
// そのコンポーネントでしか使用しない色をハードコーディング
light: '#f4f5f6',
dark: '#323233',
}),
bg: theming({
// 定義済みの汎用カラー(グローバル変数)を参照することも可能
light: c => c.blue_800,
dark: c => c.blue_100,
}),
}
return css`
color: ${colors.text};
background-color: ${colors.bg};
`
})
```
上記のように定義できる `theming` 関数を用意したことで、グローバルスコープの色指定が増え過ぎることを避け、そのコンポーネント内だけでテーマ別の色指定を完結できるようになりました。
また、`TypeScript` の型チェックやコード補完を利用することで、必ず `light / dark` の両方が色指定されていることを自動的にチェックできたり、定義済みの汎用カラーを補完候補から簡単に選べるようにもなりました。
現在の DocBase では、上述の `ThemeProvider` コンポーネントと `theming` 関数の2種類を使い分けて、テーマの切り替えを実現しています。
## よかったこと
### 先にライト・ダーク含めた汎用カラーを定義しておいたので、ダークテーマ対応が楽になった
フロントエンド改修の際に汎用カラーのライト・ダークテーマもある程度定義していました。
その際に、既存コンポーネントでもなるべく汎用カラーを使うよう修正し、新規コンポーネントも汎用カラーを使うようにしました。
(もちろん汎用カラーを使っていないコンポーネントもありますが)
このおかげで、ダークテーマ対応を始めたときには、汎用カラーを使ったコンポーネントはほとんど手を加える必要はありませんでした。
またなるべく汎用カラーを使うようにしたことで、デザインデータにありがちな「見た目はほとんど同じ色なのにカラーコードが違う」ミスをほぼ無くせました。

コンポーネントの色を決めるときはなるべく定義済みの Coloer styles を使用します
### Storybook があった
フロントエンド改修の際に、主要なコンポーネントは Storybook に登録していました。
こちらのおかげで確認作業がだいぶ楽になりました……!

### Color の変数を TypeScript オブジェクトにしたので候補から楽に選べる
「テーマ切り替えの実装」で書いていただいたように、Color の変数を TypeScript のオブジェクトとして定義しています。そのため、styled-components で利用する際に`${theme.}`を打ち込んだだけで定義済みの変数がサジェストされるようになりました。
typo も防げますし、直感的に記述できるので、かなり開発体験もよくなったと思います!

## 大変だったこと
### メモ本文の文字色を試行錯誤

DocBase には文字色を指定する機能があります。
選択した文字列を``で囲むシンプルなものです。
それらの色は、ダークテーマに切り替えたときに著しく可読性が落ちるため、調整する必要がありました。

ダークテーマにした状態。暗い
#### ダークテーマ時には文字色を反転→色相だけ戻す
こちらはエンジニアの方に「**js で文字色を反転して、輝度を暗い背景に合わせたあと、色相だけ元に戻せばいいのでは?」**とアイデアをいただきました。
白背景で可読性の高い文字色の輝度を反転させたら、そのまま暗い背景で見やすい輝度になるわけです。
実際には以下のように実装してもらいました。
1. メモ本文内の`style`属性で`color`が指定されている`span`要素を抽出
1. style属性で指定された`color`の値を反転
- 輝度の値が反転、色相が180度変わります
1. 色相を再度180度回転させて元の色相に戻す
実装としては`Polished`ライブラリを使い、該当のカラーコードに`complement(invert(${textColor}))`をかけています。
[Polished](https://polished.js.org/) とは、`Sass` の `Mixins` や `functions` を React でも使えるライブラリです。`invert`は指定のカラーコードを反転した値、complementは補色(いわゆる反対色)を返します。
そして After がこちら!

**う〜〜〜〜ん!** 緑とか紫は見やすくなったけどグレーとかピンクはまだ見づらい!
単純に反転+色相移動では足りないようです。
DocBase はテキストを読むサービスなので、文字と背景のコントラスト比`4.5:1`は達成したいところです。
#### まずはライトテーマを微調整
そもそも、ライトテーマでもいくつかコントラスト比`4.5:1`を達成していないものがあるので、合わせて調整しました。
(2番目のグレーは `disable` 状態など、読ませることを意識していない想定だったので対象外としています)
← Before After →
 
色の違いは少しわかりづらくなりましたが、見やすくなりました!
ピンクやオレンジ、黄色は明度を下げすぎると別の色になってしまうので、だいぶ調整に苦労しました。。
また、彩度が低いと反転したときに色の判別が難しくなるので、彩度を高めにしています。
#### ダークテーマの文字色調整
お次はダークテーマで文字色を反転したときの調整です。
`complement(invert(${textColor}))`したときの色が全体的に暗く感じたので、Polishedライブラリで使えるlighten関数を追加しました。
`lighten`は、指定した値に応じて明るくしたカラーコードを返してくれます。
値は0~1の間で指定できるのですが、この調整もけっこう悩みました。。
明るくすればするほど可読性は上がるのですが、同時に白っぽくもなっていくため、色の判別がつきづらくなるデメリットがありました。
ひたすらブラウザ上で値の調整とリロードを繰り返し、最終的には以下の値になりました。
(この作業は丸1日ぐらいやっていた気がします……)
```tsx
lighten(0.06, complement(invert(${textColor})))
```
そして実際の見え方はこちら!
← Before After →
 
全体的に見やすくなりましたね! :clap:
デザイナーも意外と泥臭い作業をしています。
## 終わりに
デザインの観点からダークテーマ対応について語ってみました。
個人的にはデザイン作業を Figma でやったことでかなり効率が上がったと思います。
ダークテーマについて、「ここが見づらい」などご意見があればお気軽にサポートまでご連絡ください!