私たちはKotlinMultiplatformでアプリを開発しています。
クロスプラットフォームでの設計と実装を進める中で、UI設計において避けては通れないのが「デザインシステム」の選定です。
その中でもよく採用されるのが、Googleが提唱する「Material Design」。この記事では、
- なぜマテリアルカラーを採用しなかったのか
- 代わりにどのようにテーマを構築したのか
について、実体験をもとにお話しします。
1. そもそもマテリアルカラーとは?
マテリアルデザインは、Googleが提唱するデザインシステムで、UI設計におけるガイドラインやコンポーネント、そして配色ルールを提供します。
その中でも「マテリアルカラー」は、
-
primary
(主要色) -
secondary
(補助色) -
surface
やbackground
といった背景用の色
など、“役割ベース”のカラー命名 が特徴です。
💡 利用するメリットは?
- UI全体に統一感が出る
- デザイナーとエンジニアの共通言語になる
- アクセシビリティ(視認性)の基準を満たしやすい
つまり、迷わず使える。そう、迷わなければ。
2. なぜマテリアルカラーを使わなかったのか
正直なところ、最初はマテリアルカラーを使っていました。けれど、次第に違和感を覚えるようになったのです。
2.1 命名がフィットしない
たとえば、リンクの色。
「このリンクの色は primary(青)と同じ。でも、これをprimaryと呼ぶのは何か違う……」
マテリアルの命名は「役割ベース」ですが、私たちのUI設計は意味ベース だったのです。進捗バーの青、完了マークの緑、注意喚起の赤──色はその“意味”を伝えるためのものでした。
2.2 デザイン思想とのズレ
マテリアルデザインは、「色でユーザーを誘導する」思想に基づいています。ボタンやFABが浮かび上がり、アクセントカラーで行動を促す設計です。
でも、私たちのアプリでは、あくまで「コンテンツが主役」。
色やUIが主張しすぎず、ユーザーがコンテンツに集中できる静かな世界観を重視していました。操作が直感的であれば、派手なボタンで誘導する必要はないのです。
その結果、マテリアルカラーは UI の意図と噛み合わず、命名や実装の両面で迷いが生じてしまったのです。
3. カスタムテーマ、こう実装しました
マテリアルカラーを使わないと決めた以上は、自分たちで配色ルールを設計する必要があります。そこで、以下の方針に基づいてカスタムテーマを実装しました。
3.1 カラースキームの構造定義
まずは、アプリ内で使う色をカテゴリごとに整理し、それぞれのスキーム(色設計)をデータクラスとして定義します。
data class ColorSchemes(
val layout: LayoutColorScheme,
val dialog: DialogColorScheme,
val text: TextColorScheme,
)
data class TextColorScheme(
val title: Color,
val link: Color,
)
-
ColorSchemes
は、全体のカラースキーマをまとめたエントリーポイント。 - 各セクション(
layout
,dialog
,text
)で使う色をスキームごとに分けています。
3.2 明暗テーマ別のカラー定義
ライトモード/ダークモードに応じて、それぞれのテーマ用カラーを定義します。
fun lightTextColors(): TextColorScheme =
TextColorScheme(
title = Color(0xFF000000),
link = Color(0xFF007AFF),
)
fun darkTextColors(): TextColorScheme =
TextColorScheme(
title = Color(0xFFFFFFFF),
link = Color(0xFF8AB4F8),
)
※ lightLayoutColors()
や lightDialogColors()
は省略していますが、同様に定義します。
3.3 テーマごとの具体的なカラースキームを構築
val CustomLightColors = ColorSchemes(
layout = lightLayoutColors(),
dialog = lightDialogColors(),
text = lightTextColors(),
)
val CustomDarkColors = ColorSchemes(
layout = darkLayoutColors(),
dialog = darkDialogColors(),
text = darkTextColors(),
)
3.4 グローバルで使えるテーマオブジェクトの提供
val LocalCustomColors = staticCompositionLocalOf { CustomLightColors }
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val colors = if (darkTheme) CustomDarkColors else CustomLightColors
CompositionLocalProvider(LocalCustomColors provides colors) {
content()
}
}
object CustomTheme {
val colors: ColorSchemes
@Composable
get() = LocalCustomColors.current
}
-
MyAppTheme
でテーマを切り替え、CustomTheme.colors.xxx
として各所で使用可能になります。
3.5 UIへの適用例
Text(
text = text,
color = CustomTheme.colors.text.title,
)
このように、明確な意味に基づいた命名でカラーを管理することで、「primary か secondary か」といった迷いはなくなり、この色は何のためにあるのか がコード上でも明確になります。
おわりに
マテリアルカラーは非常に優れた仕組みです。しかし、それがすべてのプロジェクトにフィットするとは限りません。
UIに強い色の主張がいらない。意味で色を使い分けたい。そんなときには、自分たちのルールで配色を設計するのも、立派な選択肢です。
デザインと実装の“しっくりくる”を求めて、私たちはカスタムテーマを選びました。
同じように悩んでいる方の参考になれば嬉しいです。