UIコンポーネントにおけるInteraction StatesとState Layerの考察

この記事は Goodpatch Advent Calendar 9日目の記事です。アドベントカレンダー2つ目の記事をなんとか書くことができた大角です。

今回はインタラクティブなUIコンポーネントの状態変化において、一貫性を持ちながら、効率よく実装するために「Interaction States」と「StateLayer」の概念を理解し、それをどのように実装するかについて考察します。

Interaction Statesとは

Interaction Statesは、インタラクティブなUIコンポーネントにおいて「状態」を伝えるための視覚表現のことです。たとえば、ボタンはインタラクティブなコンポーネントですが、hoverの状態やactiveの状態で背景色を変えることにより、ユーザーに対してフィードバックを与えています。

いくつかのデザインシステムでは、Interaction Statesの定義が記載されていますが、今回はその中でもレイヤー構造を用いて、視覚表現をシステマチックに設計しているものに着目します。

Interaction StatesにおけるState Layer

一部のデザインシステムでは、状態変化に「レイヤー構造」を用いることによって、一貫性と効率性を持ちながら、システマチックに状態を設計しています。

レイヤー構造は、以下の3つのレイヤーから構成されます。(各レイヤーの名前は私が勝手に命名したものです🙏)

  • ContentLayer:テキストなどのコンテンツを配置するレイヤー
  • StateLayer:状態によってスタイルを変えるレイヤー
  • BaseLayer:UIコンポーネント固有のスタイルを持つレイヤー

StateLayerでは、状態ごとに透明度を変えることによって背景色が変化したように見えるように設計されています。StateLayerは通常時は不可視状態=透明度0%ですが、hover状態では15%、active状態では25%のように透明度を変更します。

ここでは、StateLayerとBaseLayerが分離されていることがポイントです。StateLayerとBaseLayerを分割することによって、背景色が変わったとして、StateLayerは同じルールを適用することができます。

たとえば、Material Design 3のInteraction Statesでは、以下のようなレイヤー構造が定義されています。

Interaction states – Material Design 3

A state layer is a semi-transparent covering on an element that indicates its state. State layers provide a systematic approach to visualizing states by using opacity. A layer can be applied to an entire element or in a circular shape and only one state layer can be applied at a given time.

状態レイヤーは、各状態の不透明度に対して固定されたパーセンテージを使用します。状態レイヤーは、コンテンツによって使用される色(通常はオン色)と、それぞれの状態の不透明度のパーセンテージを使用します。

他にも、Goldman Sacksのデザインシステムでもレイヤー構造を用いたInteraction Statesが定義されています。

Interaction States - Goldman Sachs Design

Interaction Statesの実装

それでは、試しにレイヤー構造を用いたInteraction Statesを実装してみたいと思います。できるだけシンプルにするために標準のHTMLとCSSを用いて、汎用的な「ボタン」のUIコンポーネントを実装してみたいと思います。

レイヤーを定義する

まずはHTMLを定義します。3つのレイヤー構造を実現するためにbuttonタグ内にspanタグを含めるようにしています。

<button type="button" class="button">
    <span>Button</span>
</button>

レイヤー構造はCSSのpositionを用いて、階層化します。記述をシンプルにするために余計なスタイルは記載していません。

:root {
    --button-base-color: #2146c7;
    --button-content-color: #fff;
}

.button {
  /* BaseLayer */
  position: relative;
    background: var(--button-base-color);
  border-color: var(--button-base-color);
  color: var(--button-content-color);
}

.button > span {
  /* ContentLayer */
  position: relative;
  z-index: 0;
}

.button::before {
  /* StateLayer */
  content: "";
  position: absolute;
  z-index: 0;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background: currentColor; /* StateLayerの色はContentLayerのcolorと合わせる */
}

状態変化に合わせて、StateLayerの透明度を変更する

前述で定義したStateLayerに対して、hover/active/focus-visible時に透明度のopacityを変更します。

.button::before {
  /* 省略 */
  opacity: 0;
}

.button:hover::before {
  opacity: 0.15;
}

.button:active::before,
.button:focus-visible::before {
  opacity: 0.25;
}

これで、StateLayerによって状態ごとに視覚表現を変えることができるようになります。

ボタンに複数のVariantを定義する

多くの場合、ボタンは重要度や優先度によって、複数のスタイルを定義します。そのため、ボタンのvariantとして、Fill/Outline/Textの視覚的に異なるボタンを定義します。

まずはvariantごとに利用する色を定義します。

:root {
  /* Fill */
  --button-fill-base-color: #2146c7;
  --button-fill-content-color: #fff;
  /* Outline */
  --button-outline-base-color: #fff;
  --button-outline-content-color: #404258;
  /* Text */
  --button-text-base-color: transparent;
  --button-text-content-color: #404258;
}

Modifierクラスによって、variantのスタイルを適用します。

.button.fill {
  background: var(--button-fill-base-color);
  border-color: var(--button-fill-base-color);
  color: var(--button-fill-content-color);
}

.button.outline {
  background: var(--button-outline-base-color);
  border-color: var(--button-outline-content-color);
  color: var(--button-outline-content-color);
}

.button.text {
  background: var(--button-text-base-color);
  border-color: var(--button-text-base-color);
  color: var(--button-text-content-color);
}

以上で3つのvariantを持つボタンが出来ました。

ここでのポイントはvariantごとにhoverやactiveの状態に対するスタイルを定義していないことです。StateLayerで状態を共通化することによって、variantごとの状態の定義が不要になります。

ボタンに複数のThemeを定義する

昨今はMediaQueryを利用することによって、Webサイトでもライトモード/ダークモードに対応することが可能になりました。

ここでは、ライトモード/ダークモードによってボタンの色を変更します。

@media (prefers-color-scheme: light) {
  :root {
    --button-fill-base-color: #2146c7;
    --button-fill-content-color: #fff;
    --button-outline-base-color: #fff;
    --button-outline-content-color: #404258;
    --button-text-base-color: transparent;
    --button-text-content-color: #404258;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --button-fill-base-color: #3758cc;
    --button-fill-content-color: #fff;
    --button-outline-base-color: transparent;
    --button-outline-content-color: #fff;
    --button-text-base-color: transparent;
    --button-text-content-color: #fff;
  }
}

MediaQueryによってライトモードとダークモードの場合でBaseLayerの背景色を少し変えています。

ライトモード/ダークモードのようなテーマの切り替えは、ユーザーにとって好ましい一方で色設計やCSSの定義も考慮することが増えます。しかし、StateLayerを用いることによってテーマが変わったとしても変更点を抑えることができます。

実装デモ

上記で実装した結果は実装デモのページでご確認いただけます。 ライトモード/ダークモードの切り替えは、Google Chromeの場合はDevToolsを利用することで確認できます。

Chrome DevToolsのprefer-color-schemeをdarkに変更する

おわりに

いかがでしたでしょうか。最近ではデザインシステムの文脈でもマルチテーマの議論が進んでいますし、今後のWebサイトやアプリケーションではライトモード/ダークモードの対応がより重要になると思われます。

複数のテーマを扱う場合は色指定は単一ではなく、複数の色セットが定義されることが当たり前になります。そのような場合には、レイヤー構造を用いることによって、状態変化をシンプルに設計することが可能になるのではないかと思いました。

今回の記事では、Interaction StatesとStateLayerの概念および実装について考察しましたが、実際にプロダクションで試したものではないため、不足している部分があるかもしれません。もし、他の方法や改善点があれば、Twitterやコメントで教えていただけると幸いです。