マテリアルデザイン 3 のカラースキームを Material Color Utilities で作る
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
とします。取得する色は surface
・surfaceContainer
・onSurface
・primary
・primaryContainer
の 5 色とします(執筆時点で取得できる色は全部で 49 色 あり、必要な色を取得します)。また、追加する静的色(旧カスタム色) として、青の #0000ff
を success
という名前で追加します。これらの色からカラースキームを生成します。
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);
結果です。light
と dark
がキーのオブジェクトです。各色は 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 では SchemeFruitSalad
と SchemeRainbow
は export
されておらず使えません。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
は違う色のスキームもあります。例えば、SchemeRainbow
と SchemeTonalSpot
、SchemeVibrant
のライトモードの primaryContainer
は同じ色ですが、onPrimaryContainer
は全て異なります。
SchemeRainbow |
|
---|---|
SchemeTonalSpot |
|
SchemeVibrant |
|
必要に応じて 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 | ライト
| ダーク
|
---|---|---|
コントラストレベル 0.5 | ライト
| ダーク
|
コントラストレベル 0.0 | ライト
| ダーク
|
コントラストレベル -0.5 | ライト
| ダーク
|
コントラストレベル -1.0 | ライト
| ダーク
|
コントラストレベルを変更しても surface
の色は変わりません。一方、primary
と onPrimary
は、コントラストレベルに応じて色が変わります。primary
のコントラスト比は、文字色の onPrimary
に対しても背景色の surface
に対しても 1
が最も高くなり、コントラストレベルが低くなるにつれコントラスト比も低くなります。また、コントラスト比が最低となる -1
であっても文字色の onPrimary
に対するコントラスト比は 4.5:1 以上になり、背景色の surface
に対するコントラスト比は 3:1 以上になります。ボタンコンポーネントの Filled button や チェックボックスコンポーネント などで primary
を使いますが、背景色に対するコントラスト比が必ず 3:1 以上になるためアクセシビリティを考慮したサイトにできます。
primary
はコントラストレベルに応じて分かりやすい変化をするのですが、primaryContainer
の変化は分かりにくいです。onPrimaryContainer
と surface
に対する primaryContainer
のコントラスト比を確認してみます。
コントラストレベル 1.0 | ライト
| ダーク
|
---|---|---|
コントラストレベル 0.5 | ライト
| ダーク
|
コントラストレベル 0.0 | ライト
| ダーク
|
コントラストレベル -0.5 | ライト
| ダーク
|
コントラストレベル -1.0 | ライト
| ダーク
|
ライトモードで文字色の 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 を有効にします。
調和の有無でどの程度色が変わるか試してみます。原色は赤の #ff0000
、追加色は青の #0000ff
で success
という名前です。カラースキームは SchemeTonalSpot
、コントラストレベルは 0
です。
success | ライト・調和あり
| ライト・調和なし
| ダーク・調和あり
| ダーク・調和なし
|
---|---|---|---|---|
successContainer | ライト・調和あり
| ライト・調和なし
| ダーク・調和あり
| ダーク・調和なし
|
調和ありの色は調和なしの色と比較し、原色の赤に近い色になります。