🎨

FigmaのVariablesからFlutterのColorSchemeまで!変更に強い色設計フローを実案件で試した話

に公開

🔥 はじめに

アプリ開発において、FigmaなどのデザインツールからFlutterへの落とし込みで「カラー設計をどう実装・管理すべきか?」という課題に直面したことはありませんか?

特に以下のような悩みは、実務でもよく聞かれます。

  • 複数人開発でカラーの使い方がブレてしまう
  • 色定義の粒度がバラバラで実装に迷う
  • ダークモードやブランドカラー変更に柔軟に対応できない

この記事では、実際の案件で試して効果を感じた「FigmaのVariables → FlutterのColorScheme設計フロー」をベースに、

  • Primitive token(色の原子)
  • Semantic token(意味を持つ色)
  • ThemeExtension(Flutterの拡張テーマ)

といった概念を交えながら、変更に強くチームで扱いやすいカラー設計の方法を私なりにまとめます。

TL;DR

  • Figma では Primitive → Semantic の2階層で “意味づけ” しておく
  • Flutter では Semantic を ColorScheme に直マップ、足りない分を ThemeExtension に逃す
  • こうすると ブランドカラー変更もダークモードも 1 箇所修正で完結
  • theme_tailor で copyWith/lerp 問題もさよなら!

🫡 【結論から紹介】私のカラー設計フロー

Figma側

Figmaでは以下のようにカラー変数(Variables)を階層構造で管理します。

PrimitiveToken(例: gray100, orange500)  

SemanticToken(例: primary, surface, onPrimary)

SemanticTokenは、最終的にFlutterのColorSchemeへとマッピングされるよう設計しています。

画像はFigmaのvariablesで定義しているSemantic Token。
Semantic Token を階層管理している Figma 画面

Flutter側

Flutterでは、Material Design準拠のColorSchemeを軸にSemanticTokenを定義。
必要に応じて、ThemeExtensionで独自定義(例: inactive, onInactive)を追加しています。

Theme.of(context).colorScheme.primaryのように利用することで、UI側の実装も統一され、メンテナンス性が向上します。

class AppColorScheme {
  // ライトテーマ
  static const light = ColorScheme(
    brightness: Brightness.light,
    primary: Color(0xFFFE782F),
    onPrimary: Color(0xFFFFFFFF),
    secondary: Color(0xFFEEC223),
    onSecondary: Color(0xFFFFFFFF),
    error: Color(0xFFEE2323),
    onError: Color(0xFFFFFFFF),
    surface: Color(0xFFFFFFFF),
    onSurface: Color(0xFF1E1E1E),
    outline: Color(0xFFFE782F),
    outlineVariant: Color(0xFFA2A2A2),
  );

  // ライトテーマ専用の ThemeExtension
  static const colorSchemeLightX = AppColorSchemeX(
    inactive: Color(0xFFAFAFAF),
    onInactive: Color(0xFFFFFFFF),
  );

  // ダークモードに対応する場合は以下
  // static const dark = ColorScheme(
  //   brightness: Brightness.dark,
  //   primary: Color(0xFFFE782F),
  //   onPrimary: Color(0xFFFFFFFF),
  //   secondary: Color(0xFFEEC223),
  //   onSecondary: Color(0xFFFFFFFF),
  //   error: Color(0xFFEE2323),
  //   onError: Color(0xFFFFFFFF),
  //   surface: Color(0xFFFFFFFF),
  //   onSurface: Color(0xFF1E1E1E),
  //   outline: Color(0xFFFE782F),
  //   outlineVariant: Color(0xFFA2A2A2),
  // );
  // static const colorSchemeLightX = AppColorSchemeX(
  //   inactive: Color(0xFFAFAFAF),
  //   onInactive: Color(0xFFFFFFFF),
  // );

⚙️ Design Tokenでカラーを定義する意味

FigmaなどのDesign Systemであらかじめカラーを定義する理由は明確です。

  • アプリ / Web / 広告 などで横断的に統一感ある世界観を出せる
  • チーム間で「使っていい色(ダメな色)」が共有できる
  • ブランド変更にも柔軟に対応できる

このとき、まず定義されるのがPrimitive tokenと呼ばれる原始的な色たちです(例:gray100, blue500など)。

この辺りのTokenについてもっと詳しく知りたい方は以下のFigma公式の動画をご覧ください!
動画冒頭で、なぜPrimitive Tokenを作成するのかなどが視覚的に紹介されていて、かなりわかりやすいです!

https://www.youtube.com/watch?v=1ONxxlJnvdM

🎯 Flutterで扱うべきはSemantic Token?

Design Systemでよく登場するのが Primitive Token(gray100 や blue500 などの純粋な色定義)ですが、Flutterでそのままコード化するのは避けたほうが良いと感じています。

理由はシンプルで、

どの色を使えばいいのかわからない😅🌀

たとえば gray100 や gray300 のような色を自由に使えてしまうと、意図しない色の使い分けが生まれ、デザインの統一感が崩れやすくなります。

✅ Semantic Tokenの導入が効果的

Semantic Token とは、「意味のある用途にひもづいた色」のことです。

  • gray100 → backgroundSubtle
  • blue500 → buttonPrimary

というように「この色は何のためにあるのか?」を明確にした定義がSemantic Tokenです。

FlutterではこのSemantic Tokenを ColorScheme や ThemeExtension にマッピングしておくことで、設計意図がブレない UI 実装が可能になります。

🔧 Primitive Tokenはあえてコードにしない

Primitive Token自体をコードにしてしまうと、再び「gray100 vs gray200問題」が発生します。
そのため、コード化するのはSemantic Tokenのみに限定するのがベターだと感じています。

  • Primitive Token(gray100, blue500 など)はFigma上の変数にとどめておく
  • Semantic Token(primary, surfaceなど)だけをFlutterで定義

このルールを定めることで、実装者が迷わずに済み、設計の意図を守ったまま実装できます。

🎨 FlutterのColorSchemeクラスについて

Flutterでは ThemeData.colorScheme を使うことで、マテリアルデザインに沿った色の定義ができます。
これは以下のような利点があります👇

ColorSchemeで色を指定するメリット

  1. マテリアルデザインとの親和性が高い
    • primary, secondary, error, background などの意味があらかじめ定義されている
    • 各UIコンポーネントが自動的にそれらの色を使用してくれる(例:FilledButtonの背景色はprimary)
  2. Light / Dark Theme 両対応がしやすい
    • 同じ ColorScheme に対して、ThemeMode.light / ThemeMode.dark に応じて色を切り替えられる
  3. 設計ルールを定着させやすい
    • 意味ベースで colorScheme.surface のように指定することで「なぜこの色か」が明確になる(=Semantic Token)

ColorSchemeの実装例

AppColorSchemeXの実装については後述します。

このクラス内でcolorSchemeLightXを記載することで、カラー定義が1ファイルにまとまるところが個人的にはおすすめポイントです💡

class AppColorScheme {
  // ライトテーマ
  static const light = ColorScheme(
    brightness: Brightness.light,
    primary: Color(0xFFFE782F),
    onPrimary: Color(0xFFFFFFFF),
    secondary: Color(0xFFEEC223),
    onSecondary: Color(0xFFFFFFFF),
    error: Color(0xFFEE2323),
    onError: Color(0xFFFFFFFF),
    surface: Color(0xFFFFFFFF),
    onSurface: Color(0xFF1E1E1E),
    outline: Color(0xFFFE782F),
    outlineVariant: Color(0xFFA2A2A2),
  );

  // ライトテーマ専用の ThemeExtension
  static const colorSchemeLightX = AppColorSchemeX(
    inactive: Color(0xFFAFAFAF),
    onInactive: Color(0xFFFFFFFF),
  );

  // ダークモードに対応する場合は以下
  // static const dark = ColorScheme(
  //   brightness: Brightness.dark,
  //   primary: Color(0xFFFE782F),
  //   onPrimary: Color(0xFFFFFFFF),
  //   secondary: Color(0xFFEEC223),
  //   onSecondary: Color(0xFFFFFFFF),
  //   error: Color(0xFFEE2323),
  //   onError: Color(0xFFFFFFFF),
  //   surface: Color(0xFFFFFFFF),
  //   onSurface: Color(0xFF1E1E1E),
  //   outline: Color(0xFFFE782F),
  //   outlineVariant: Color(0xFFA2A2A2),
  // );
  // static const colorSchemeLightX = AppColorSchemeX(
  //   inactive: Color(0xFFAFAFAF),
  //   onInactive: Color(0xFFFFFFFF),
  // );

🤔 AppColorsクラスなどではダメか?

もちろん、すべての色を AppColors.primary のように定義して使う設計もできます。
(私も使っていたし、プロジェクトによっては利用する)

ただし、それでは以下のような問題が発生します。

  • ボタンやカードなどのマテリアルウィジェットが自動的に ColorScheme を使ってくれなくなる
  • Themeの切り替え時(特にダークモード)に自前で切り替え処理を実装する必要がある
  • ThemeData に色がバインドされていないため、チーム開発で一貫性が崩れやすくなる

そのため、基本的には ColorScheme に semantic tokenに該当する色のみを登録し、それ以外を拡張したい場合に次の方法が役立ちます👇

😡 【番外編】そもそもColorSchemeってどこに利用されているかわかりづらい!

わかります。
そんな方のために、以下のソースコードおよびWebアプリを用意しています!
READMEを確認して実際に色を変更してみてください!

▼ GitHub
https://github.com/go5go69/flutter-color-scheme-sample

▼ Webサイト
https://flutter-color-scheme-sample.web.app/

ThemeExtension(Flutterの拡張テーマ)を利用する

Flutter 3.0以降では、ThemeExtension を使って ColorSchemeでは定義できない独自のテーマプロパティを拡張できます。

🧩 どんな時に使う?

success, warning, info などマテリアルで定義されていないカラーなどを追加できます。

また、今回は色について記事なので詳細は省略しますが、Padding, BorderRadius, Shadowなど、UIスタイルの定義も可能です。

✍️ ThemeExtensionの実装例

マテリアルデザインではカバーされない「独自の意味を持つ色」には、ThemeExtensionを使って拡張します。

@TailorMixin()
class AppColorSchemeX extends ThemeExtension<AppColorSchemeX>
    with _$AppColorSchemeXTailorMixin {
  const AppColorSchemeX({required this.inactive, required this.onInactive});

  @override
  final Color inactive;
  @override
  final Color onInactive;
}

theme_tailorを使えば、copyWithやlerpの定義を自動生成できるので管理も楽です。
詳細は以下をご覧ください!
https://pub.dev/packages/theme_tailor

💡 ブランドカラーの変更やダークモード対応に強くなる理由

この設計の最大のメリットは、変更に強いアーキテクチャになることです。

✅ ブランド変更に対応しやすい

たとえばブランドカラーが#FE782F → #2D9CDBに変更になったとしても、AppColorScheme.light.primaryの値を変更するだけ。
ColorSchemeに基づいて構成されたUIなら、全体が一貫して置き換わります。

これは特に開発初期段階でカラーリングがFIXしていないけど、スケジュール的に実装はしていきたい...みたいな時に有効だったと感じています!

🌗 ダークモードも1定義で切り替え

ThemeMode.lightとThemeMode.darkの切り替えに応じて、ColorScheme.light()とColorScheme.dark()を切り替えるだけ。
手動で色を変える必要はほぼなくなります。

🧠 まとめ

  • FigmaではPrimitive Token → Semantic Tokenの順でColorを設計
  • FlutterではSemantic Tokenに該当するものだけをColorSchemeで管理
  • 足りない色はThemeExtensionで拡張して柔軟に管理
  • theme_tailorを使えば、拡張テーマのメンテも簡単
  • ブランドカラーやダークモード対応も一元化でき、設計がブレない

最後に

何か不明点や間違っている部分などあればコメントください🙇🏻

Discussion