🎨

Reactとtailwindでデザインシステムっぽいものの第一歩を踏んでみる

2022/12/15に公開

はじめに

こんにちは!今年の4月にフロントエンドエンジニアとして働き始めたyamakenjiです。

もう12月が来ていることにびっくりしており、社会人になってからあっという間に時間が経っているなと実感しております。

振り返ってみると今年は本当にいろんなことを経験した年で、最近はフロントエンドだけでなく、SwiftやKotlinといったモバイル周りやPHP、AWS周りのインフラ構築など、機能を1から提供するために必要なことは全て触っていたりします。

さて、昨今ではデザインシステムが話題になっていたりします。その中でよく耳にするのが

  • デザインシステムを構築したはいいものの、どうやって周知しようか
  • 運用・メンテナンスどうしようか

といった、構築後の長期的な運用目線が多いかなと思います。確かに、デザインシステムは作ったら終わりではなく、継続的に運用して組織やプロダクトに合わせて成長させていく必要があります。
とはいうものの、じゃあ実際に1から構築する時にどこから手をつけたらいいのか少しイメージがつきずらい部分もあったりしました。

そこで本記事では、デザインシステムっぽいものの最小構成を1から作ってみて、構築や運用のイメージを掴むための第一歩を踏んでみようかなと思います。
なお、デザインシステムを構築していく中でReactやtailwindも出てきますが、それらの基本的な概要は割愛させていただきます。
https://github.com/yamakenji24/simple-designsystem

デザインシステム

そもそもデザインシステムとは何か、人や組織の課題によってはデザインシステムの定義が異なったりします。本記事では、組織やプロダクトのミッションを実現するための一貫したデザインガイドであり、そのブランドを運用・保守するための仕組みと定義します。

デザインシステムを構成する要素として主に3つ挙げられます。

  1. デザイン原則
  2. スタイルのガイドライン
  3. UIコンポーネントライブラリ

デザイン原則

デザイン原則とは、組織やプロダクトのミッションを実現することができる良いデザインである、かなと思います。曖昧な書き方にはなっていますが、どのような課題を解決したいのかによって良いデザインの基準が変わるんだろうなと思っています。
これらの大事にしている価値観を言語化し、共有していくことがデザインの道標になるのかなと思いました。

デザイン原則については、以下の記事がとても参考になります。
https://yasuhisa.com/could/article/design-principles-decisions/

例えば、SmartHRでは、「SmartHRらしさ」、「SmartHRサービスのビジョン」を表現するために4つの基本原則を定義しています。
https://smarthr.design/foundation/

スタイルのガイドライン

スタイルのガイドラインでは、色やタイポグラフィ、スペース、インタラクションのルールなど、プロダクトのスタイルを統一するための要素を定義します。
例えば、色などはデザイン原則に基づいて、その組織やプロダクトのブランドとなる色を定義したりすることが多いと思います。
共通で利用するスタイルを定義したドキュメントや、それらを使用するためのデザイントークンをライブラリとして提供したりします。

UIコンポーネントライブラリ

UIコンポーネントライブラリは、デザイン原則やスタイルに基づいて、ボタンやフォーム、ダイアログといった、すぐに再利用可能な共通で利用すべきUIの単位です。

共通コンポーネントを利用することで、チーム間でのコンポーネントの粒度や見た目を統一でき、スタイルに関するコミュニケーションや開発の効率化ができます。

企業によっては、OSSとして公開している場合もありますので、それらを参考にしたりして非常に勉強になりました。
https://github.com/kufu/smarthr-ui

最小構成で構築していく

最小構成として、いくつかのデザイントークンとUIコンポーネントとしてボタンを実装していきます。
モジュールバンドラーとして、microbundleを利用していきます。
ゼロコンフィグで利用でき、簡単にcjs,esmを出力でき、かつTypeScriptにも対応している点が非常によかったです。

構成としては以下のようになっており、packages以下に各デザインに関連するライブラリを定義しています

  • @config: 共通で利用できそうな設定値
    • 今回はtailwindの設定を共通で利用します
  • @foundation: デザイントークン集
  • @ui: 共通で利用するUIコンポーネント集

なお、今回はnpmレジストリの方にはあげずにnpm workspacesを利用してローカルで擬似的に利用してみます。

$ tree -L 4 -I node_modules
.
├── examples
├── package.json
├── packages
│   ├── @config
│   │   └── tailwind
│   ├── @foundation
│   │   └── theme
│   └── @ui
│       └── components
├── postcss.config.js
├── tailwind.config.js
├── tsconfig.base.json
└── tsconfig.json

共通のtailwind configを利用するためのpackage作成

今回はデザイン周りにtailwindを導入していきます。ユーティリティファーストでcssを定義することができ、それらをtailwind.config.jsで共通化し、チーム間でのデザインの一貫性を保つために非常に強力になると考えられます。

そのためには、デザインシステム側で共通したスタイルを設定したconfigを用意し、それらを配布する必要があります。

tailwindのcustimizationに合わせたオブジェクトを書きます。
なお、デザイントークンとかぶる部分もありますが、ここのconfigの設定ではあくまでプロダクトデザインとして共通で利用する基本的なスタイルの一覧を定義し、役割に応じたスタイルへの命名などをデザイントークンで行っていきます。

../@config/tailwind/src/tailwindConfig.ts
import type { Config } from "tailwindcss";

type TailwindConfig = Omit<Config, 'content'>
const createTailwindConfig = () => {
  return {
    theme: {
      colors: {
        // 例:ブランドカラーを定義
        'blue': '#1fb6ff',
	'gray': '#8492a6',
	...
      }, 
      spacing: {
        '1': '8px',
	'2': '12px',
	...
      },
      ...
    },
    plugins: [],
  };
};

export const config: TailwindConfig = createTailwindConfig();

これらをmicrobundleを利用してbundleし、ライブラリとしてimportして利用できるようにします。

../@config/tailwind/package.json
{
  "name": "@config/tailwind",
  "sideEffects": false,
  "type": "module",
  "source": "./src/index.ts",
  "main": "./dist/index.cjs",
  "module": "./dist/index.module.js",
  "types": "./dist/index.d.ts",
  "exports": {
    "require": "./dist/index.cjs",
    "default": "./dist/index.modern.js"
  },
  "scripts": {
    "build": "microbundle --no-compress -f modern,esm,cjs",
    "clean": "rimraf dist"
  },
  "devDependencies": {
    "microbundle": "^0.15.1",
    "rimraf": "^3.0.2",
    "tailwindcss": "^3.2.4",
    "typescript": "^4.9.4"
  }
}

exampleとして利用してみる

rootのnpm workspacesで定義しているので、localでimportして利用してみます。

package.json
{
  "name": "simple-designsystem",
  "version": "1.0.0",
  "workspaces": [
    "packages/@foundation/*",
    "packages/@ui/*",
    "packages/@config/*"
  ],
  "scripts": {
    "build": "npm run build --workspaces",
    ...,
  },
  "devDependencies": {
    ...,
  }
}
examples/package.json
{
  "name": "examples",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    ...,
    "@foundation/theme": "file:../packages/@foundation/theme",
    "@ui/components": "file:../packages/@ui/components",
    "@config/tailwind": "file:../packages/@config/tailwind",
  },
  "devDependencies": {
    "autoprefixer": "^10.4.13",
    "postcss": "^8.4.19",
    "tailwindcss": "^3.2.4"
  }
}

tailwind.config.jspresetsに任意の設定を差し込めるみたいなので、これを利用していきます。

example/tailwind.config.js
const { config } = require("@config/tailwind");

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [...],
  presets: [config],
};

デザイントークン用のpackage作成

例えば、ボタンのインタラクションに利用する色を定義したいみたいな場合やタイポグラフィー用など、以下のような構成が1案として考えられるかなと思います。

これらをユーザ側が利用したり、UIコンポーネント側が利用したりします。

@foundation/theme/src/colors.ts
/**
 * ボタン用に利用するカラー
 */
export const buttonColors = {
  primary: {
    base: "bg-sky-500",
    active: "bg-sky-400",
    disabled: "disabled:bg-gray-500",
    hovered: "hover:bg-sky-300",
  },
  danger: {
    base: "bg-red-500",
    active: "bg-red-400",
    disabled: "disabled:bg-gray-500",
    hovered: "hover:bg-red-300",
  },
} as const;
export type ButtonColors = typeof buttonColors;

/**
 * タイポグラフィー用に利用するカラー
 */
 export const typographyColors = {} as const;

UIコンポーネント用のpackage作成

ここでは、簡易的なボタンコンポーネントを実装していきます。
ボタンのpropsとして一般的なHTMLButtonElementを受け取る他に、variantやsizeといったボタンの状態を表すようなものを受け取ります。
これらに応じてボタンの色やサイズを動的に変更します。

../@ui/components/src/Button/index.tsx
import React from "react";
import { buttonColors } from '@foundation/theme'

type Variant = "default" | "primary" | "danger";
type Size = "s" | 'm' | 'l';

export interface Props
  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "type"> {
  variant: Variant;
  size: Size;
}
export const Button = React.forwardRef<HTMLButtonElement, Props>(
  function Button(
    { children, variant = "default", size = "s", disabled = false, ...rest },
    ref
  ) {
    const styles = styleToUtilities(variant, size);

    return (
      <button {...rest} disabled={disabled} ref={ref} className={styles}>
        {children}
      </button>
    );
  }
);

const styleToUtilities = (variant: Variant, size: Size) => {
  const BASESTYLES = "justify-center items-center"
  const variantCSS = variantToUtilities(variant);
  const sizeCSS = sizeToUtilities(size);
  return [BASESTYLES, variantCSS, sizeCSS].join(' ');
}

const variantToUtilities = (variant: Variant) => {
  switch (variant) {
    case "default":
      return "";
    case "primary":
      const primary = buttonColors.primary;
      return `${primary.base} ${primary.hovered}`;
    case "danger":
      const danger = buttonColors.danger;
      return `${danger.base} ${danger.hovered} ${danger.disabled}`;
    default:
      return "";
  }
};
const sizeToUtilities = (size: Size) => {
  switch(size) {
    case 's':
      return "text-sm h-8 p-1";
    case 'm':
      return "text-base h-12 p-2";
    case 'l':
      return "text-lg h-16 p-3";
    default:
      return "";
  }
}

実際に開発する時やドキュメントとして残すという意味合いでもstorybookも活用します。

microbundleを用いてjsxをbundleする時、デフォルトだとpreact前提で作られているためjsxをh関数に変換します。そのため、ReactのコードをビルドするにはオプションのjsxFactoryでcreateElementを指定してあげる必要があります。

../@ui/components/package.json
{
  "name": "@ui/components",
   ..., 
  "scripts": {
    "build": "microbundle --no-compress -f modern,esm,cjs --jsx React.createElement --jsxFragment React.Fragment",
    "clean": "rimraf dist"
  },
  "dependencies": {
    "@foundation/theme": "1.0.0",
  },
}

実際に、提供されたUIコンポーネントを利用する場合は以下のようになります。

../examples/src/App.tsx
import { Button } from '@ui/components'

function App() {
  return (
    <div>
      <header>
        <Button variant='danger' size='m'>
          button from components
        </Button>
        <p className="bg-yellow-500">test text</p>
      </header>
    </div>
  );
}

export default App;

まとめ

デザインシステムっぽいものを最小構成で構築してみました。
本記事を執筆しながら、「あれ? 実はデザイントークンも使えるただのUIコンポーネントライブラリなのでは?」とか思ったりしました。また、デザイントークンでもボタンの色などを付けましたが、何色をつけようか迷いました。

これは、デザインシステムの構成要素であるデザイン原則が定まってないから発生したものだと考えており、デザイン原則をまずは定義してあげることがデザインの軸を固めるという意味でも非常に重要なのだとわかりました。

今回、最小構成で作ってみて、1から構築していく時のイメージが少しついたような気がします。
また冒頭でも述べたように、構築して終わりではなく、いかに運用して改善していくかが重要になってきます。
そのためにもフィードバックを集める仕組みづくりやそれをもとに改善する体制を整えたりする必要もあるのかなと思いました。
最後に、microbundle なかなか良い!

GitHubで編集を提案

Discussion