🎨

ReactでMaterial Youのような動的なカラーパレットを実現する

2023/01/17に公開

Material YouはMaterial Design 3に含まれるデザインコンセプトで、アプリケーションの見た目や操作感をユーザーが自由にカスタマイズできるものです。
https://material.io/blog/announcing-material-you

Androidでは既にネイティブでこの仕組みを利用することができますが、Webでも@material/material-color-utilitiesというnpmモジュールとして、Material Design 3のColor System部分を導入することができます。

インストール

npm install @material/material-color-utilities

または

yarn add @material/material-color-utilities

https://www.npmjs.com/package/@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,
   },
 ]);


Color system

テーマを割り当てる

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でカラーパレットを使うためのuseMaterialColorHooksを作ってみました。

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