クロスプラットフォームデザインシステム、1.5年の記録(1)
クロスプラットフォームデザインシステム、1.5年の記録
FEConf2023で発表した「クロスプラットフォームデザインシステム、1.5年の記録」をまとめた記事です。発表内容を2回に分けて公開します。第1回ではデザインシステムとデザイントークンについて学び、コンポーネントの構成要素を理解してデザイナーと開発者のコミュニケーション問題を解決します。第2回では第1回の内容を基にコンポーネントの実装とAPI設計を行います。本文に挿入されている画像の出典はすべてこのコンテンツと同じタイトルの発表資料で、個別の出典は記載していません。発表資料はFEConf2023のウェブサイトからダウンロードできます。
▶2024年FEConfセッション発表者募集中
▶2024年FEConfライトニングトーク発表者募集中
FEConf2023で発表された「クロスプラットフォームデザインシステム、1.5年の記録」/ハテヨン カラット フロントエンドエンジニア
こんにちは。カラットでデザインシステムの構築と運用を担当しているハテヨンです。この記事では、私が1年半にわたってデザインシステムを構築する中で試みた様々なアプローチと、その過程で陥った落とし穴、そしてそれらの経験から学んだことを共有したいと思います。
1. デザインシステム
デザインシステムの目標
デザインシステムを構築する際、「デザインシステム」という言葉が非常に広いスペクトラムを持っており、これによりチーム間のコミュニケーションエラーやチームの方向性が曖昧になる経験をすることがあります。この章では、これを解決するためのデザインシステムの方向性を明確に設定し、デザインシステムの設計目標を正確に把握する方法、目標設定時に陥りがちな落とし穴への対策について学んでいきます。
下の図は、オープンソースのデザインシステムとして有名なChakraとAdobeのSpectrumデザインシステムです。同じ機能を持つレンジスライダーを実装するにも、大きな違いがあります。Chakraはすべてのサブコンポーネントを明示する必要がある一方、Spectrumはレンジスライダー1行だけで済みます。
なぜこのような違いが生まれたのでしょうか?私の考えでは、2つのデザインシステムが追求する価値が異なるためだと思います。Chakraは「オープンソースを活用する開発者が柔軟に使用できるライブラリ」という目的で作られ、SpectrumはAdobe製品群で使用する目的で作られました。Chakraは柔軟性をより重要な価値と考え、Spectrumは一貫性をより重要な価値と考えているため、このような違いが生まれたのです。
このような理由から、私は2つのデザインシステムを異なる視点で捉え、Chakraのようなデザインシステムを「汎用デザインシステム」と呼び、Spectrumのようなデザインシステムを「プロダクト言語」と呼んで区別しています。このようなプロダクト言語には、Spectrum以外にもMaterial、Carbon、Fluentなど多くの種類があります。では、なぜ多くの企業が汎用デザインシステムを使用してデザインシステムを構築せず、デザインシステム組織を別途設けてプロダクト言語を作るのでしょうか?
その理由は、UIにブランディングやアプリの特性など、企業固有の意思決定が反映される必要があるためです。プロダクト言語は、一般的なデザインシステムの機能に加えて、「意思決定の集合を管理する」という意味が追加されています。より強調して言えば、プロダクト言語の本質はデザイン意思決定の集合であり、UIライブラリはそれを実装する手段に過ぎないと言えます。
企業が外部ライブラリを使用できるのは、そのライブラリが意思決定に関係がないか、意思決定をそのまま反映できる場合です。しかし、UIライブラリが意思決定を反映できない場合が多く、これにより多くの企業がデザインシステム組織を設けてプロダクト言語を作ることになります。
落とし穴:デザインシステムの目標設定
私はデザインシステムを構築する際、いくつかの落とし穴に陥りました。まず、目標を明確に設定せず、参考にする対象を誤って判断しました。カラットアプリに必要なデザインシステムはプロダクト言語であり、モバイルが最優先で、Web、iOS、Androidの実装がすべて必要です。私はチームを構成する初期にレスポンシブWebを対象とするデザインシステムを参考にしましたが、これはレスポンシブWebには適していても、私たちのプロダクトには不適切なアプローチとなってしまいました。
もう一つの落とし穴は、1つのデザインシステムで複数の環境とプロダクトをカバーしようとした欲張りさでした。サービスの成長に伴い、モバイルアプリだけでなくウェブサイトや管理画面など、より多くのプロダクトをサポートする必要が生じました。ブランディングはすべてのプロダクトで概ね同じであるため、よく作られた汎用デザインシステム1つで全てのプロダクトをカバーしようとしました。しかし、このような試みは不可能か、プロダクト言語を作るよりも大きなコストがかかる状況を生み出しました。
この経験を通じて、完璧な汎用デザインシステムを作るよりも、どのようなプロダクト言語を作るにしても繰り返し現れる方法論が存在し、それを活用することがより重要であることを学びました。その中で最も重要なのは「デザイン意思決定の集合をプロダクトデザインに関連するすべての領域に効果的に同期するシステム」です。私はこれを冗談めかして「デザインシステム-システム」と呼んでいます。次の章では、このようなデザイン意思決定を効果的に管理し同期するための「デザイントークン」について学んでいきます。
2. デザイントークン
この章では以下の内容について学びます。
- デザイントークンが持つ意味の理解
- デザイントークンの活用事例の学習
- デザイントークン運用時の落とし穴への対策
デザインシステムを構築することは、結局のところデザイン決定の連続です。「ボタンコンポーネントの高さを40pxにする」「ブランドカラーを#ff6f0fにする」など、すべてがデザイン決定に該当します。
そして、ほとんどのプロダクトがそうであるように、意思決定は時間とともに変化するものです。プロダクトが成長するにつれて、より良いUIとは何かを新たに学ぶためです。では、このような意思決定の変化を最も早くすべてのプロダクトに反映する方法は何でしょうか?
それは、意思決定を機械が読み取れる形式でエンコードし、プロダクトでそれを読み取って反映するアプローチです。この章のタイトルでもある「デザイントークン」は、先ほどの説明のように、デザイン意思決定を人と機械の両方が読み取れる形式でエンコードすることを意味します。
それでは、デザイントークンについてより詳しく見ていきましょう。
値のエンコード
まず、デザイントークンは値をエンコードすることができます。下の図のデザイントークンはどのような意味を持っているでしょうか?
「この色の名前は
2つの表現は似ているように見え、言葉遊びのように思えるかもしれませんが、ここには重要な違いがあります。プロダクトでこの色を使用するという意味は、宣言されていない色は使用しないという意思を含んでいます。これにより、デザインシステムチームはデザイン決定を柔軟な集合として維持し、デザイン意思決定を効果的に管理することができます。ただし、デザインの柔軟性と自由度は低下する可能性があります。
世の中には無限の概念が存在しますが、私たちは有限の単語だけで十分にコミュニケーションを取りながら生きていくことができます。私は同じ文脈で、無限のデザイン値が存在しても、有限のデザイントークンでコミュニケーションを取ることができると信じています。
意図のエンコード
デザイントークンをより柔軟に使用するために、階層を追加することもできます。以下のように値をエンコードして有限に構成されたデザイントークンに意図をエンコードする階層を追加できます。このデザイントークンの表現は「プライマリブランドを意図する領域に$carrot-500を使用する」という意図をエンコードしています。
このように階層を追加することで、現在は同じ色で表現されていますが、将来的に異なる色で表現される可能性があるという意思決定を示すことができます。また、値ではなく意図に基づいてデザインする思考方法を追求することで、プロダクトデザイン過程での意思決定とコミュニケーションを改善します。
コンテキスト
最後に、デザイントークンはコンテキストに応じて異なる値を与えることができます。例えば、同じ$carrot-500という値にライトモードとダークモードという分岐を追加できます。
先ほどの3つをまとめると、デザイントークンは以下のようにコンテキストと階層を通じて構成されます。コンテキストと階層を活用することで、デザイントークンはプロダクトデザインに使用するのに十分な表現力を持つようになります。
カラットでは、この定義に基づいて簡単なDSL(Domain-Specific Languages:ドメイン特化言語)を作成し、デザイントークンを管理しています。この表現式は今年追加されたFigma Variablesと意味的に一致するため、新しくデザイントークンを構築するチームがあれば、これを積極的に検討するのも良いでしょう。カラットでは、Figmaコンポーネントフレーム内にこのDSLを配置し、このコンポーネントを信頼できる情報源として使用しています。このアプローチにより、Figmaへのドキュメント同期を自動化したり、ダークモードプレビューなどを実装しています。
また、コンポーネントをデプロイする際にwebhookを追加してアクションをトリガーし、これによりFigmaを信頼できる情報源として使用し、各プラットフォーム別のコードを生成してデザイン決定をクロスプラットフォームで同期しています。
デザイントークンをコードに
フロントエンドでの適用方法をより詳しく見ていきましょう。下のスタイルシートでは、データ属性を通じてコンテキスト分岐を実装し、CSS Variableでデザイントークンと階層を実装しています。
そして、CSS in JSでは使いやすくするためにCSS Variableをエイリアスするパッケージを一緒に生成しています。
このエイリアスパッケージは、emotionでは下の最初の図のように使用でき、vanilla-extractでは2番目の図のように使用できます。組織によって使用される技術が多様なカラットの特性上、特定のCSS in JS技術のみを適用することはできないため、以下のように様々なアプローチで技術に合わせて適用されています。
落とし穴:セマンティックトークンの早計な抽象化
デザイントークンを作る際に苦労した部分は、技術的な部分よりも運用面でした。特に記憶に残る誤った判断は、セマンティックトークンを性急に定義したことです。例えば、下のようなFABコンポーネントがある場合、このコンポーネントの背景色はフローティングの意味を持つと言えるでしょうか?
私の経験から考えると、このような早計なセマンティックトークンの定義は、有用に使用されるよりも、より大きなデザインの混乱を引き起こすことが多かったです。
では、有用なセマンティックトークンを定義する方法は何でしょうか?以下のように様々なユースケースを考慮し、どのような関心事を共有しているかを検討することです。Input、Text area、Number Inputなど様々なInput Fieldが存在し、これらすべてが同じ背景色を共有する必要がある場合、それらの背景色をField-bgと定義すれば、有用なセマンティックトークンとして使用できます。
セマンティックトークンは、適切に定義して使用すれば確かに強力なツールとなります。しかし、誤って定義されたセマンティックトークンは、より大きな混乱を引き起こす可能性があります。したがって、性急にセマンティックトークンに意図を与えることは危険です。つまり、抽象化は観察を通じて得られるため、複数の事例を共有する共通の関心事を検討し、それを通じて意図を抽出するアプローチの方が安全です。
これまでデザイントークンについて学びました。デザイントークンはデザインシステム組織を運用する際に非常に強力な手段ですが、ユーザーがデザインシステムに期待するのはコンポーネントでしょう。次の章では、デザインシステムにおけるコンポーネントについて学んでいきます。
3. コンポーネント
この章では以下の内容について学びます。
- Atomic Designが混乱を招く理由
- コンポーネントの構成要素の正確な理解
- デザイナー-開発者間のコミュニケーション問題の解決
Atomic Design
Atomic Designは数年前まではよく言及されていましたが、いつの間にかあまり聞かれなくなったキーワードです。皆さんはAtomic Designを特別な変形なしにプロダクトにうまく適用できていますか?少なくとも私は、Atomic Designをそのまま適用するのは難しいと感じています。
下の図のラジオグループコンポーネントで、どの部分がアトミックな単位でしょうか?
実際、答えは一つに定まりません。基準によって答えが変わるためです。形態単位では黄色のボックスがアトムですが、機能単位ではラジオボタン1つでは意味がないため、青いボックスの機能単位がアトムです。そして、アクセシビリティ単位ではフォーム要素のレベルを提供する義務があるため、赤いボックスのアクセシビリティ単位がアトムとなります。
このように機能と形態を分けて捉えるアプローチについて、簡単に見ていきましょう。
コンポーネント:形態
下のチェックボックスは、選択されていないときは空のボックスの形態を持ち、選択されているときはチェックが入ったボックスで表現されます。このように、現在の状態に対応するスタイルをレンダリングすることが、コンポーネントの形態的側面です。
コンポーネント:機能
下の図のように、チェックボックスはクリックによって選択状態を切り替えることができます。この場合、チェックボックスがどのように見えるかは重要ではありません。チェックボックスの順序が変わったり、色が異なったり、トグルスイッチのようにチェックボックスではない完全に異なる形態になる可能性もあります。
したがって、レンダリングを除去して状態だけを残すと、コンポーネントの機能的側面がより明確になります。つまり、コンポーネントの機能的側面は、レンダリングを除いて、ユーザー入力に対してコンポーネントの状態がどのように遷移するかを定義します。
Atomic Designが混乱を招く理由
では、Atomic Designが混乱を招く理由は何でしょうか?結論として、アトミックな機能、アトミックな形態は存在し得ます。しかし、アトミックなコンポーネントは一つに定まりません。したがって、コンポーネントだけを分類して最小単位として定義しようとすると、失敗しやすくなります。形態的アトムを提供するスタイルシートと機能的アトムを提供するヘッドレスコンポーネントを分けて考えることで、何が最小単位かという混乱をなくし、チームが前に進むことができます。では、Atomic Designから離れて、機能と形態それぞれについてコンポーネントの構成要素を解剖していきましょう。
コンポーネントの解剖:機能
まず、コンポーネントの機能は構造、状態、相互作用、コンテキストで構成されます。
まず、構造はコンポーネントがどのような部品で構成され、各部品がユーザー相互作用にどのような役割を持つかを定義します。例えば、上のコンポーネントはコントロールという役割を持つチェックボックスとラベルという役割を持つItem Labelテキストで構成され、これらを包むRootがあります。
2つ目に、状態はユーザー入力によって変化し得るコンポーネントの状態を定義します。代表的なものにPressed(active)、Hover、Focused、Selected(Checked)などがあります。
3つ目に、相互作用は状態の変更を引き起こす単位です。チェックボックスを例に取ると、クリックするとCheckedが変わり、タブでフォーカスを当てるとFocusedが変わります。
最後に、コンテキストはコードで注入するオプションで、動作に関与します。例えば、Disabledオプションを注入するとチェックボックスが無効化され、いくらクリックしてもCheckedが変わりません。もちろん状態も動作に関与できますが、状態はユーザーの入力によって変更されるものであり、コンテキストはコードによってのみ変更されるという重要な違いがあります。
コンポーネントの解剖:形態
次に、コンポーネントの形態について学びましょう。コンポーネントの形態は構造、視覚オプション、状態オプション、デザイン決定で構成されます。
まず、構造について学びましょう。形態における構造は機能における構造と多くの共通点がありますが、必ずしも一致するわけではありません。ここでの構造は、コンポーネントがどのような部品で構成され、各部品がどのようなレイアウトを持つかを定義します。例えば、アイコンは機能的には存在しませんが、形態的には存在します。Figmaでは、コンポーネントのフレーム構造をこれに可能な限り近く構成することが望ましいです。
2つ目に、視覚オプションは設定されたオプションに応じて形態が変わることを定義します。Size、Variantなどが代表的な例です。
3つ目に、状態オプションは先ほど説明した機能から生じる状態とコンテキストによって形態が変わることを定義します。Hovered、Focused、Checkedなどが代表的な例です。
最後に、このように定義された状態オプションと視覚オプションのすべての組み合わせについて、各構造ごとにどの属性にどのデザイン値が与えられるかをすべて整理したものが、このコンポーネントに対するデザイン決定です。そして、このデザイン決定をより効果的に表現する手段としてデザイントークンを使用できます。
これまで機能と形態それぞれについてどのような構成要素を持つかを学びました。まとめると、機能の構造と形態の構造は大きな共通点を持ちますが、互いに異なる可能性があり、機能の状態とコンテキストは形態の状態オプションとほぼ正確に対応しますが、ここにはまだ問題が残っています。
機能と形態構造の問題点
デザイナーの立場での状態オプションは左のように5つの場合の数しか持ちません。しかし、開発者の立場での状態とコンテキストの組み合わせは2の4乗で合計16通りの場合の数を持つ可能性があります。私は初期にFigmaにもこのようなすべての場合の数を描くべきだと主張したこともあります。当然ながら歓迎されず、実際にも不要な作業でした。
状態がさらに増えると、下のように組み合わせ的な状態爆発が発生し、制御できないほどの場合の数が生じることもあります。
しかし、私たちは経験的に状態間に優先順位が存在することを知っています。例えば、Disabledのときはホバーを考慮する必要がありません。実際にレンダリングが変わる必要がある状態の組み合わせは限定的で、その限定的な状態はデザイナーの視覚で捉えた場合の数と一致するでしょう。したがって、状態とコンテキストの組み合わせを条件に応じて1次元のenumに整理して状態を圧縮することで、コンテキストと状態オプションを漏れなく把握し、コミュニケーションを取ることができます。
関心の分離
このように状態圧縮まで完了すると、機能と形態の共通の関心事と固有の関心事を把握できます。構造、状態、コンテキストは両方に共通して存在する関心事であり、相互作用は機能にのみ、視覚オプションとデザイン決定は形態にのみ存在する関心事です。このように関心事を分離することで、どのような利点が得られるでしょうか?
コミュニケーションの循環参照
関心事の分離は、コミュニケーションの循環参照を解決するキーワードです。以下のような状況を考えてみましょう。
開発者:デザインがないのでまだコンポーネントを実装できません。
デザイナー:コンポーネントをデザインしたいのですが、どのようなVariantが必要ですか?
デザイナー:とりあえずデザインしたのですが、開発者がこのままでは作れないと言っています。
開発者:でもすでにFigmaにデプロイしてデザイナーたちが使っています。
このような状況をコミュニケーションの循環参照問題と呼びます。実際にデザインシステムやコンポーネント単位で開発する組織で発生し得る問題です。私はこのような問題を「デザインを先にしてコンポーネントを実装する方式の問題」と考えています。デザインをする際に開発にどのような状態が存在することになるかを事前に知るのは難しいため、開発への依存性が生まれます。一方、開発をする際にはデザインが出てこないとコードを書けないと考えているため、デザインへの依存性が生まれます。
コミュニケーションの循環参照解決
では、互いに依存的なコミュニケーションの循環参照をどのように解決できるでしょうか?開発ではよくインターフェースを使用して依存の方向を逆転させる方法を使用します。これは私たちの働き方にも適用できます。私たちは先ほど言及した関心事の分離に基づいて、デザインと開発の共通インターフェースが何であるかを知っています。これに基づいて、私が考えるデザインシステムチームの働き方を紹介します。
まず、作成しようとするコンポーネントの簡単なスケッチをデザイナーと開発者が一緒に描き、どのような構造と状態オプションを持つかを定義します。下の図が該当コンポーネントで使用される共通インターフェースです。
開発者は与えられた構造からユーザーアクセスの利便性を考慮して機能的に意味のある構造だけを残し、ユーザー相互作用に応じた状態遷移を把握します。これによりヘッドレスコンポーネントを直接実装するか、すでにあるライブラリを持ってきます。
デザイナーは状態オプションに基づいて構造に合わせてコンポーネントをデザインします。
最後に、開発者はデザインされたコンポーネントに基づいてスタイルを書き、すでに書いておいたヘッドレスコンポーネントと結合して機能と形態をすべて実装します。そして、Figmaと同期されているコンポーネントを完成させます。
まとめると、先ほど言った「デザインを先にしてコンポーネントを実装する方式の問題」、つまりFigmaでデザインした内容が開発につながる方式で働くのではなく、構造や状態などの共通インターフェースを先に定義し、この内容がFigmaと開発につながる方式に変えることで、コミュニケーションの循環参照を解決できます。
この過程を通じて私たちが学べる点があります。結局、Figmaも開発と同様に環境として捉え、開発と同様に実装体として扱うことが実際の働き方により適しているということです。
これまでコンポーネントをどのように定義し、どのようにデザインと開発が一緒に作業できるかを学びました。次の記事では、上記の内容を実際のコンポーネント実装にどのように適用できるかを学んでいきます。
Discussion