🎨
ReactでMaterial Youのような動的なカラーパレットを実現する
Material YouはMaterial Design 3に含まれるデザインコンセプトで、アプリケーションの見た目や操作感をユーザーが自由にカスタマイズできるものです。
Androidでは既にネイティブでこの仕組みを利用することができますが、Webでも@material/material-color-utilities
というnpmモジュールとして、Material Design 3のColor System部分を導入することができます。
インストール
npm install @material/material-color-utilities
または
yarn add @material/material-color-utilities
テーマを生成する
テーマは特定の色から生成するか、画像から生成するかの二つの方法があります。
import { argbFromHex } from "@material/material-color-utilities";
// 特定のカラーから生成する
const sourceColor = argbFromHex('#f82506');
const theme = themeFromSourceColor(sourceColor);
// 画像から生成する
const image = document.createElement('img');
image.src = /* 画像のURL等 */;
const theme = themeFromImage(image);
またカスタムカラーを指定することで、別途そのカラーに基づいたテキストカラーや、コンテナカラーを生成することもできます。
themeFromSourceColor(seedColor, [
{
name: "custom-color",
value: argbFromHex("#ff0000"),
blend: true,
},
]);
テーマを割り当てる
applyTheme
を使ってテーマを割り当てることができます。
import { applyTheme } from "@material/material-color-utilities";
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
applyTheme(theme, {
target: document.body, // HTML要素を指定して、テーマの適応範囲を決める
dark: systemDark // システム側のダークモード設定に合わせてダークテーマを適応するかを決める
});
テーマを使う
Javascriptから直接カラーにアクセスする場合
const colorScheme = systemDark ? 'dark' : 'light';
const scheme = theme.schemes[colorScheme];
const primary = scheme.primary;
// カスタムカラーへのアクセス
const customColor = theme.customColors[0][colorScheme].color;
CSSからテーマを使う場合、--md-sys-color-
プリフィックスを付けてアクセスします。
.something {
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
}
Custom Hooks
最後にReactでカラーパレットを使うためのuseMaterialColor
Hooksを作ってみました。
import {
argbFromHex,
themeFromImage,
themeFromSourceColor,
Theme,
applyTheme,
} from '@material/material-color-utilities';
import { useState, useEffect, useRef } from 'react';
type ApplyColorOptions = {
imageSrc?: string;
colorHex?: string;
dark?: boolean | undefined;
target?: HTMLElement | undefined;
brightnessSuffix?: boolean | undefined;
paletteTones?: number[] | undefined;
};
export const useMaterialColor = (opts: ApplyColorOptions = {}) => {
const [isApplying, setIsApplying] = useState(false);
useEffect(() => {
if (!opts.imageSrc && !opts.colorHex) return;
apply(opts);
}, []);
const theme = useRef<Theme>();
const apply = async ({
imageSrc,
colorHex,
dark,
target = document.body,
brightnessSuffix,
paletteTones,
}: ApplyColorOptions) => {
setIsApplying(true);
if (!imageSrc && !colorHex) {
throw Error('You must specify either imageSrc or colorHex');
}
if (imageSrc) {
const image = document.createElement('img');
image.src = imageSrc;
theme.current = await themeFromImage(image);
} else {
theme.current = themeFromSourceColor(argbFromHex(colorHex));
}
setIsApplying(false);
return theme.current;
};
return { isApplying, apply, theme: theme.current };
};
Discussion