メインコンテンツまで移動する

マテリアルデザイン 3 のカラースキームを Material Color Utilities で作る

  • 公開日

Akira Web デザイナー

TypeScript 版

マテリアルデザイン 3 のカラースキーム を生成できるのが Material Color Utilities です。いくつかの言語 で使うことができますが、この記事では TypeScript 版を使う方法を書いています。

TypeScript 版の Material Color Utilities を使うにあたって注意すべきは、カラースキームを生成する方法が 2 つあることです。1 つは、マテリアルデザイン 3 がリリースされた当初のカラースキームを生成する themeFromSourceColor()。もう 1 つは、リリース後に追加された色を含めて生成できる MaterialDynamicColors です。この記事では執筆時点での最新のカラースキームを生成する MaterialDynamicColors を使います。

themeFromSourceColor() から MaterialDynamicColors への移行は theme helper for new dynamic schemes! が参考になります。themeFromSourceColor() と同じ返値になるように書かれているため、変更を最小限に抑えることができます。

色を作る

Material Color Utilities を使うために、まずはインストールします。

npm i @material/material-color-utilities

マテリアルデザイン 3 のカラースキームは、任意の 1 つの原色を与えれば自動的に様々な役割別の色を生成します。

その原色は赤の #ff0000 とします。取得する色は surfacesurfaceContaineronSurfaceprimaryprimaryContainer の 5 色とします(執筆時点で取得できる色は全部で 49 色 あり、必要な色を取得します)。また、追加する静的色(旧カスタム色) として、青の #0000ffsuccess という名前で追加します。これらの色からカラースキームを生成します。

import {
  Blend,
  Hct,
  MaterialDynamicColors,
  SchemeContent,
  SchemeExpressive,
  SchemeFidelity,
  SchemeMonochrome,
  SchemeNeutral,
  SchemeTonalSpot,
  SchemeVibrant,
  argbFromHex,
  hexFromArgb,
} from '@material/material-color-utilities';

type Role = M3Colors['roles'][number];
type Additions = { name: string; color: `#${string}`; isHarmonize: boolean }[];
type Scheme = keyof M3Colors['schemeClasses'];

class M3Colors {
  private source: `#${string}`;

  private sourceArgb: number;

  private sourceHct: Hct;

  private additions: Additions;

  private scheme: Scheme;

  private contrastLevel: number;

  /**
   * マテリアルデザイン3の色
   * @param source - 原色
   * @param additions - 追加色
   * @param scheme - 使用するスキーム
   * @param contrastLevel - コントラストレベル 最小は-1 最大は1 デフォルトは0
   */
  constructor(
    source: `#${string}`,
    additions: Additions = [],
    scheme: Scheme = 'SchemeTonalSpot',
    contrastLevel: number = 0.0,
  ) {
    const hexRegEx = /^#[0-9A-F]{6}$/i;
    if (!hexRegEx.test(source)) {
      throw new Error(`Invalid color value: ${source}`);
    }
    const invalidAddition = additions.find(
      (addition) => !hexRegEx.test(addition.color),
    );
    if (invalidAddition) {
      throw new Error(
        `Invalid additional color value: ${invalidAddition.color}`,
      );
    }
    if (contrastLevel < -1.0 || contrastLevel > 1.0) {
      throw new Error(
        `Invalid contrastLevel value: ${contrastLevel}. It should be between -1.0 and 1.0 inclusive.`,
      );
    }
    this.source = source;
    this.sourceArgb = argbFromHex(this.source);
    this.sourceHct = Hct.fromInt(this.sourceArgb);
    this.additions = additions;
    this.scheme = scheme;
    this.contrastLevel = contrastLevel;
  }

  /**
   * 取得する色
   */
  private roles = [
    'surface',
    'surfaceContainer',
    'onSurface',
    'primary',
    'primaryContainer',
  ] satisfies Exclude<
    keyof typeof MaterialDynamicColors,
    | 'contentAccentToneDelta'
    | 'highestSurface'
    | 'primaryPaletteKeyColor'
    | 'secondaryPaletteKeyColor'
    | 'tertiaryPaletteKeyColor'
    | 'neutralPaletteKeyColor'
    | 'neutralVariantPaletteKeyColor'
    | 'prototype'
  >[];

  /**
   * 使用可能なスキームの一覧
   * SchemeFruitSaladとSchemeRainbowは0.2.7では使用不可
   * https://github.com/material-foundation/material-color-utilities/commit/f9bda5647a2ab8e489faa251ac7841524b58a6ae
   */
  private schemeClasses = {
    SchemeContent,
    SchemeExpressive,
    SchemeFidelity,
    SchemeMonochrome,
    SchemeNeutral,
    SchemeVibrant,
    SchemeTonalSpot,
  };

  /**
   * 使用するスキームのインスタンスを作成
   * @param sourceHct - 原色(追加色の場合は追加色)のHCT色空間
   * @param isDark - ダークモード用はtrue ライトモード用はfalse
   * @returns スキーム
   */
  private getScheme(sourceHct: Hct, isDark: boolean) {
    const SchemeClass = this.schemeClasses[this.scheme];
    return new SchemeClass(sourceHct, isDark, this.contrastLevel);
  }

  /**
   * 原色に基づくベースラインのカラースキームを生成
   * https://m3.material.io/styles/color/advanced/adjust-existing-colors#c6810874-a320-4684-8df6-3869887ea49c
   * @returns ライトモード用とダークモード用のオブジェクト
   */
  private generateBaselineColors() {
    const lightScheme = this.getScheme(this.sourceHct, false);
    const darkScheme = this.getScheme(this.sourceHct, true);
    return this.roles.reduce(
      (acc, cur) => {
        acc.light[cur] = hexFromArgb(
          MaterialDynamicColors[cur].getArgb(lightScheme),
        );
        acc.dark[cur] = hexFromArgb(
          MaterialDynamicColors[cur].getArgb(darkScheme),
        );
        return acc;
      },
      { light: {}, dark: {} } as {
        light: Record<Role, string>;
        dark: Record<Role, string>;
      },
    );
  }

  /**
   * 追加色のカラースキームを生成
   * https://m3.material.io/styles/color/advanced/define-new-colors
   * @returns ライトモード用とダークモード用のオブジェクト
   */
  private generateAdditionalColors() {
    return this.additions.reduce(
      (acc, cur) => {
        const additionalColor = cur.isHarmonize
          ? Blend.harmonize(argbFromHex(cur.color), this.sourceArgb)
          : argbFromHex(cur.color);
        const additionalHct = Hct.fromInt(additionalColor);
        const lightScheme = this.getScheme(additionalHct, false);
        const darkScheme = this.getScheme(additionalHct, true);
        const onAddition = `on${cur.name.charAt(0).toUpperCase() + cur.name.slice(1).toLowerCase()}`;
        const additionalRoles: {
          role: string;
          similar:
            | 'primary'
            | 'onPrimary'
            | 'primaryContainer'
            | 'onPrimaryContainer';
        }[] = [
          { role: cur.name, similar: 'primary' },
          { role: onAddition, similar: 'onPrimary' },
          { role: `${cur.name}Container`, similar: 'primaryContainer' },
          { role: `${onAddition}Container`, similar: 'onPrimaryContainer' },
        ];
        return additionalRoles.reduce((currentAcc, { role, similar }) => {
          const modifiedAcc = { ...currentAcc };
          modifiedAcc.light[role] = hexFromArgb(
            MaterialDynamicColors[similar].getArgb(lightScheme),
          );
          modifiedAcc.dark[role] = hexFromArgb(
            MaterialDynamicColors[similar].getArgb(darkScheme),
          );
          return modifiedAcc;
        }, acc);
      },
      { light: {}, dark: {} } as {
        light: Record<string, string>;
        dark: Record<string, string>;
      },
    );
  }

  /**
   * ベースラインのカラースキームと追加色のカラースキームを結合
   */
  get token() {
    const colors = this.generateBaselineColors();
    if (this.additions.length) {
      const additionalColors = this.generateAdditionalColors();
      colors.light = { ...colors.light, ...additionalColors.light };
      colors.dark = { ...colors.dark, ...additionalColors.dark };
    }
    return colors;
  }
}

const m3Colors = new M3Colors('#ff0000', [
  { name: 'success', color: '#0000ff', isHarmonize: true },
]);
console.log(m3Colors.token);

結果です。lightdark がキーのオブジェクトです。各色は Figma プラグイン Material Theme Builder が生成する色と同じです。

{
  light: {
    surface: '#fff8f6',
    surfaceContainer: '#fceae7',
    onSurface: '#231918',
    primary: '#904b40',
    primaryContainer: '#ffdad4',
    success: '#64558f',
    onSuccess: '#ffffff',
    successContainer: '#e8ddff',
    onSuccessContainer: '#1f1047'
  },
  dark: {
    surface: '#1a1110',
    surfaceContainer: '#271d1c',
    onSurface: '#f1dfdc',
    primary: '#ffb4a8',
    primaryContainer: '#73342a',
    success: '#cebdfe',
    onSuccess: '#35275e',
    successContainer: '#4c3e76',
    onSuccessContainer: '#e8ddff'
  }
}

カラースキームの変更

先ほどのコード例では、マテリアルデザイン 3 のデフォルトのカラースキームを生成する SchemeTonalSpot を使いました。

constructor(
  // 他は省略
  scheme: Scheme = 'SchemeTonalSpot',
) {
  this.scheme = scheme;
}

カラースキームは SchemeTonalSpot 以外からも生成できます。カラースキームを生成する方法は、全部で 9 つあります。

SchemeContent
原色を微調整し Primary Container として使う。また、Primary Container はライトモードとダークモードで同じ色。
SchemeExpressive
原色から意図的に分離したスキーム
SchemeFidelity
SchemeContent と同じく原色を微調整し Primary Container として使う。SchemeContent とは Tertiary 系の色が違う。
SchemeFruitSalad
遊び心のあるスキーム。原色の色相はスキームに表示されない。
SchemeMonochrome
白・黒・灰のグレースケール
SchemeNeutral
グレースケールに近いスキーム
SchemeRainbow
遊び心のあるスキーム。原色の色相はスキームに表示されない。SchemeFruitSalad と比較し SchemeTonalSpot に近い色を生成する。
SchemeTonalSpot
デフォルトのスキーム。低から中程度のカラフルさがある。
SchemeVibrant
Primary 系のカラフルさを最大にしたスキーム

Fix exports from the MCU TypeScript library. で修正されてはいますが、執筆時点の最新版 0.2.7 では SchemeFruitSaladSchemeRainbowexport されておらず使えません。0.2.7 で使いたい場合は、/node_modules/@material/material-color-utilities/index.js を変更します。

各カラースキームがどのような色になるか primaryContainer で試してみます。原色は赤の #ff0000 です。

SchemeContent
ライト#eb0000
ダーク#eb0000
SchemeExpressive
ライト#d9e2ff
ダーク#27457f
SchemeFidelity
ライト#eb0000
ダーク#eb0000
SchemeFruitSalad
ライト#ffd7f2
ダーク#712b68
SchemeMonochrome
ライト#3b3b3b
ダーク#d4d4d4
SchemeNeutral
ライト#fddbd5
ダーク#58413e
SchemeRainbow
ライト#ffdad4
ダーク#7d2b20
SchemeTonalSpot
ライト#ffdad4
ダーク#73342a
SchemeVibrant
ライト#ffdad4
ダーク#930100

primaryContainer は同じ色でも文字色に使用する onPrimaryContainer は違う色のスキームもあります。例えば、SchemeRainbowSchemeTonalSpotSchemeVibrant のライトモードの primaryContainer は同じ色ですが、onPrimaryContainer は全て異なります。

3 つのスキームのライトモードの primaryContaineronPrimaryContainer の色
SchemeRainbow
  • primaryContainer: #ffdad4
  • onPrimaryContainer: #410000
SchemeTonalSpot
  • primaryContainer: #ffdad4
  • onPrimaryContainer: #3a0905
SchemeVibrant
  • primaryContainer: #ffdad4
  • onPrimaryContainer: #291714

必要に応じて SchemeTonalSpot を変更し、サイトに適したスキームを選びます。

// SchemeNeutralに変更する例
const m3Colors = new M3Colors('#ff0000', [], 'SchemeNeutral');

なお、Android ではユーザーがスキームを選べる設定項目があります。Web サイトでもユーザーがスキームを選べるようにしてもいいかもしれません。

コントラストレベルの変更

マテリアルデザイン 3 のカラースキームは、コントラスト比を考慮し生成されます。適切な色の組み合わせは、最低でも 3:1 以上のコントラスト比があります。

このコントラスト比は変更できます。変更するには、SchemeTonalSpot などの引数に与えるコントラストレベルを調整します。デフォルトのコントラストレベルは 0 です。最小の -1、最大の 1 の間で調整できます。

// 第4引数でコントラストレベルを変更します
const m3Colors = new M3Colors('#ff0000', [], 'SchemeTonalSpot', 0.5);

コントラストレベルを 0.5 ずつ変更し、primary のコントラスト比を確認してみます。確認するのは、文字色に使用する onPrimary とのコントラスト比と背景色に使用する surface とのコントラスト比です。原色は赤の #ff0000、カラースキームは SchemeTonalSpot です。

コントラストレベル 1.0
ライト
  • on とのコントラスト比
    15.85
  • surface とのコントラスト比
    15.1
  • primary: #44100a
  • onPrimary: #ffffff
  • surface: #fff8f6
ダーク
  • on とのコントラスト比
    20.15
  • surface とのコントラスト比
    17.81
  • primary: #fff9f8
  • onPrimary: #000000
  • surface: #1a1110
コントラストレベル 0.5
ライト
  • on とのコントラスト比
    9.92
  • surface とのコントラスト比
    9.46
  • primary: #6e3027
  • onPrimary: #ffffff
  • surface: #fff8f6
ダーク
  • on とのコントラスト比
    11.08
  • surface とのコントラスト比
    11.39
  • primary: #ffbaaf
  • onPrimary: #330502
  • surface: #1a1110
コントラストレベル 0.0
ライト
  • on とのコントラスト比
    6.42
  • surface とのコントラスト比
    6.12
  • primary: #904b40
  • onPrimary: #ffffff
  • surface: #fff8f6
ダーク
  • on とのコントラスト比
    7.75
  • surface とのコントラスト比
    10.91
  • primary: #ffb4a8
  • onPrimary: #561e16
  • surface: #1a1110
コントラストレベル -0.5
ライト
  • on とのコントラスト比
    5.32
  • surface とのコントラスト比
    5.07
  • primary: #9f574c
  • onPrimary: #ffffff
  • surface: #fff8f6
ダーク
  • on とのコントラスト比
    5.78
  • surface とのコントラスト比
    6.1
  • primary: #d07e72
  • onPrimary: #370703
  • surface: #1a1110
コントラストレベル -1.0
ライト
  • on とのコントラスト比
    4.54
  • surface とのコントラスト比
    4.43
  • primary: #aa6054
  • onPrimary: #fffbff
  • surface: #fff8f6
ダーク
  • on とのコントラスト比
    4.53
  • surface とのコントラスト比
    5.85
  • primary: #cc7b6f
  • onPrimary: #4e1810
  • surface: #1a1110

コントラストレベルを変更しても surface の色は変わりません。一方、primaryonPrimary は、コントラストレベルに応じて色が変わります。primary のコントラスト比は、文字色の onPrimary に対しても背景色の surface に対しても 1 が最も高くなり、コントラストレベルが低くなるにつれコントラスト比も低くなります。また、コントラスト比が最低となる -1 であっても文字色の onPrimary に対するコントラスト比は 4.5:1 以上になり、背景色の surface に対するコントラスト比は 3:1 以上になります。ボタンコンポーネントの Filled buttonチェックボックスコンポーネント などで primary を使いますが、背景色に対するコントラスト比が必ず 3:1 以上になるためアクセシビリティを考慮したサイトにできます。

primary はコントラストレベルに応じて分かりやすい変化をするのですが、primaryContainer の変化は分かりにくいです。onPrimaryContainersurface に対する primaryContainer のコントラスト比を確認してみます。

コントラストレベル 1.0
ライト
  • on とのコントラスト比
    9.92
  • surface とのコントラスト比
    9.46
  • primaryContainer: #6e3027
  • onPrimaryContainer: #ffffff
  • surface: #fff8f6
ダーク
  • on とのコントラスト比
    12.89
  • surface とのコントラスト比
    11.39
  • primaryContainer: #ffbaaf
  • onPrimaryContainer: #000000
  • surface: #1a1110
コントラストレベル 0.5
ライト
  • on とのコントラスト比
    4.65
  • surface とのコントラスト比
    4.43
  • primaryContainer: #aa6054
  • onPrimaryContainer: #ffffff
  • surface: #fff8f6
ダーク
  • on とのコントラスト比
    6.63
  • surface とのコントラスト比
    5.85
  • primaryContainer: #cc7b6f
  • onPrimaryContainer: #000000
  • surface: #1a1110
コントラストレベル 0.0
ライト
  • on とのコントラスト比
    13.29
  • surface とのコントラスト比
    1.23
  • primaryContainer: #ffdad4
  • onPrimaryContainer: #3a0905
  • surface: #fff8f6
ダーク
  • on とのコントラスト比
    7.2
  • surface とのコントラスト比
    1.99
  • primaryContainer: #73342a
  • onPrimaryContainer: #ffdad4
  • surface: #1a1110
コントラストレベル -0.5
ライト
  • on とのコントラスト比
    5.81
  • surface とのコントラスト比
    1.35
  • primaryContainer: #ffcdc6
  • onPrimaryContainer: #7d3b32
  • surface: #fff8f6
ダーク
  • on とのコントラスト比
    5.81
  • surface とのコントラスト比
    1.63
  • primaryContainer: #63281f
  • onPrimaryContainer: #fba395
  • surface: #1a1110
コントラストレベル -1.0
ライト
  • on とのコントラスト比
    4.53
  • surface とのコントラスト比
    1.35
  • primaryContainer: #ffcdc6
  • onPrimaryContainer: #904b40
  • surface: #fff8f6
ダーク
  • on とのコントラスト比
    4.54
  • surface とのコントラスト比
    1.63
  • primaryContainer: #63281f
  • onPrimaryContainer: #e28e81
  • surface: #1a1110

ライトモードで文字色の onPrimaryContainer とのコントラスト比が最も高くなるのは、1 ではなく 0 です。また、0.5 より -0.5 の方が文字色とのコントラスト比が高いです。surface に対するコントラスト比は、0 より -1.0 が高くなります。

コントラストレベルを 0 から変更する場合は、意図したコントラスト比になっているかの確認が必要に思えます。

なお、コントラスト比に関するアクセシビリティについては、WCAG をご参考ください。

追加色の調和

マテリアルデザイン 3 には、追加する静的色(旧カスタム色)を原色の色相に近づける 調和機能 があります。原色の色相に近づけた場合は primary などの色と調和するため、どのような色を追加してもサイト全体の色に統一感が出ます。

追加色を原色の色相に近づけて調和するのなら Blend.harmonize() を使用します。

Blend.harmonize(argbFromHex(cur.color), this.sourceArgb);

調和を有効にするか無効にするかは、追加色を指定する際に isHarmonize で調整します。

// isHarmonizeをtrueにしているためsuccess系の色は調和します
const m3Colors = new M3Colors('#ff0000', [
  { name: 'success', color: '#0000ff', isHarmonize: true },
]);

Figma プラグインでは、Extended Colors で設定する各追加色の三点リーダーをクリックし現れる Turn on harmonization を有効にします。

Turn on harmonization を有効にした場合は、追加色から生成される色は Source Color に指定した色の色相に近づきます。

調和の有無でどの程度色が変わるか試してみます。原色は赤の #ff0000、追加色は青の #0000ffsuccess という名前です。カラースキームは SchemeTonalSpot、コントラストレベルは 0 です。

success
ライト・調和あり
  • success: #64558f
  • onSuccess: #ffffff
ライト・調和なし
  • success: #555992
  • onSuccess: #ffffff
ダーク・調和あり
  • success: #cebdfe
  • onSuccess: #35275e
ダーク・調和なし
  • success: #bec2ff
  • onSuccess: #272b60
successContainer
ライト・調和あり
  • successContainer: #e8ddff
  • onSuccessContainer: #1f1047
ライト・調和なし
  • successContainer: #e0e0ff
  • onSuccessContainer: #11144b
ダーク・調和あり
  • successContainer: #4c3e76
  • onSuccessContainer: #e8ddff
ダーク・調和なし
  • successContainer: #3e4278
  • onSuccessContainer: #e0e0ff

調和ありの色は調和なしの色と比較し、原色の赤に近い色になります。

フォローする