🥨

デザインシステム作成Tips 9つ

2023/07/14に公開

はじめに

エンジニアとしてデザインシステムを立ち上げてバージョン1を作る機会があったので、そのときに学んだTipsを共有します。

そもそもデザインシステムは「生産性を上げる」ことと「統一的なUI/UXを提供する」ことが目的ですが、このTipsの内容もいずれもその目的のためのものです。

定義する面でのTips

1. コンポーネントのレベル分けを定義する

UI上ではある要素を組み合わせて別の要素が作られることがあります。例えばテキストとフレームからボタンが作られたり、ボタンを並べてページネーションを作ったり。
レベル分けしてコンポーネントそれぞれがどのレベルに属するのかを定義することで、これらの情報構造を整理することができます。アトミックデザインがその代表例ですね。

例えば以下のように分けることができます。

Level0: Styles
UIを作る最小の要素。Typography, Color, Shadow, Easing, 基準となるpxなど。
これ単体ではコンポーネントとしては使えない。

Level1: Basic Components
コンポーネントとして使える最小の要素。Button, Text, List, TextLink, Label, User Iconなど。
通常状態ではElevationの差がなく、hoverなどによって初めてElevationが変化する。

Level2: Combined Components
コンポーネントを複数組み合わせて作るコンポーネント。Dropdown, Breadcrumbs, Pagination, Toaster, Form Controll, Navigationなど。

Level3: Layout Patterns
画面全体に影響を与えるコンポーネント。Modal, Alertなど。

2. コンポーネントの上下関係を定義する

コンポーネントは重なることがあるため、どちらを上に表示するのかを考える必要があります。これをあらかじめ決めておくと、バグを防ぐだけでなくコンポーネントのあり方も整理しやすいです。
z-indexで管理するやり方もありますが、影をどれだけつけるかも合わせて考えるなら、Layerで分けてそれぞれのLayerごとにElevationがあると考えると良いと思います。

例えばGlobal NavigationはBase LayerElevation: 2、ToasterはGlobal LayerElevation: 2 みたいな考え方ですね。
この場合はどちらも Elevation: 2 に相当する影がつきますが、Toasterの方が上に表示されます。

3. コンポーネントの意味を定義する

想像以上に難しいのが、それぞれのコンポーネントの意味を定義することです。存在理由を決めると言ってもいいかもしれません。

ボタンひとつとっても、どんな色が使えるのか、アイコンを使ってもいいのか、幅は可変なのか、Tertiaryもありうるのかといった選択のバリエーションがあり、それらを決めるためにボタンがどういう存在なのかを考える必要があります。

自分が経験した例だと、Modalでアラートメッセージを表示したいので高さを小さくしたいという課題に対して、Modal自体を変えるのではなくAlertという別のコンポーネントを定義したことがありました。
Modalはどのページで表示しても同じようにModalとして認識できることが重要で、高さを小さくするとModalの位置付けがわからなくなってしまうこと、アラートメッセージを表示するときはModalとは異なり、Overlay部分をクリックしても閉じないようにすべきということから、別のコンポーネントに切り出したのでした。

どういうコンポーネントなのかはっきり認識されていると、propsや状態の定義もスムーズになるし、propsも推測しやすくなります。

抽象化の面でのTips

4. propsの名称を統一する

使いやすさにクリティカルに効いてくるのが、同じ作用をもたらすpropsの名称と値をコンポーネントをまたいで統一することです。

size というpropsなら small large なのか、 m s なのか。見た目を表すpropsとして appearance を使ったら、その値は何にするか。 position の値は leftprefix か。
こういったことを全てのコンポーネント共通で決めることで、使い慣れてないコンポーネントでもpropsの指定が快適にできるようになります。

propsで受け入れる値が変わったら、その意味も変わっていないかを意識して名称もまた考え直すのが良いと思います。
昔、外観を表すpropsとして appearance: basic | fill | outline を使っていたけど、塗りが濃いものと薄いものが出てきたときに tone: transparent | white | pale | deep に変えたことがあります。

それと普遍的すぎる名称は避けた方が無難ですね。
variant は使い勝手がいいけれど、コンポーネントごとに値がバラバラになりやすいです。

5. デザインシステム以外の文脈の用語を使わない

カプセル化ですね。
例えばIconの実装でFont Awesomeを使っている場合、Font Awesome特有の用語 (e.g. fa solid) が外から見えない形で名称を考えるのが良いですね。

サービスドメインに関わるコンポーネントの扱いは悩ましいですが、これも可能ならサービスドメインから切り離して作る方が無難だと思います。
そのほうがpropsにサービス特有の情報が露出せず、使い勝手が良くなります。

ユーザの顔写真を表示してプロフィールへのリンクを提供する UserIcon はその一例ですね。
これはデザインシステムでは ImageLink を提供して、それをラップして作ったUserIcon をプロダクト共通コンポーネントとして提供するのが良いかもしれません。

6. 過度に抽象化しない

以前、幅などに使えるpx数を定義して、 width: ${space[20]} のように使っていたことがあるのですが、値がパッとわからないしタイプ数も増えて面倒なのでやめたことがあります。
デザイン上の制約としてはピクセル数などを縛った方が良いですが、実装時には抽象化されていない方がわかりやすいものもあるので注意が必要ですね。

コンポーネントを作るときのTips

7. リセットCSSを作る

コンポーネントを意図した見た目で表示させるために、デザインシステムとしてリセットCSSも作って提供するのが良いですね。
styled-componentsであればcreateGlobalStyleでスタイルを提供できます。

import { createGlobalStyle } from 'styled-components';

/* 参考: https://github.com/Andy-set-studio/modern-css-reset/blob/master/src/reset.css */
const ResetStyle = createGlobalStyle`
  /* Box sizing rules */
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  ...
`;

export const BaseStyles = (props: Props) => {
  return (
    <>
      <ResetStyle />
      {props.children}
    </>
  );
};
app/lauout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <BaseStyles>
        <body className={inter.className}>{children}</body>
      </<BaseStyles>
    </html>
  )
}

8. Textコンポーネントを作る

テキストを受け取って、フォントサイズや色などを調整したテキストを表示するコンポーネント Text を作っておくと便利です。
headlinebody といった値でスタイルをあてられるというのもありますが、WebとMobileでサイズを変えたり、英字と日本字で別のサイズを使ったりするときには Text が必須になってきます。

デバイス情報を持つContext
import { createContext } from 'react';

export type DeviceCategoryContextType = {
  deviceCategory: 'pc' | 'mobile' ;
};

export const DeviceCategoryContext = createContext<DeviceCategoryContextType>({
  deviceCategory: 'pc',
});
Textを使うコンポーネント
export const SomeComponent = () => {
  return (
    <DeviceCategoryContext.Provider value={{ deviceCategory: 
    'mobile'>
      <div>
        <Text typography='body' color='#F0F0F0'>本文だよ</Text>
      </div>
    </DeviceCategoryContext.Provider>
  );
};
Text.tsx
export const Text = ({ typography, color }) => {
  const { deviceCategory } = useContext(DeviceCategoryContext);

  ...
}

9. ページ内での制約を考慮する

ページ内での制約を受けていて自由に使うことができないコンポーネントが少しだけあります。
そういったコンポーネントは可能ならその制約を実装に入れてしまうと、不適切な使い方を防止することができます。

例えば画面上部にぴょこっと出てくる Toaster は、一画面で一つしか表示できない制約がつくことがあります。 (複数表示できるなら Toaster Group がひとつだけと言える。)
その場合は使う際にコンポーネントを直接呼び出すのではなく、コンポーネントを表示させるhooksを呼び出す形にすると、一つしか表示させないという制約に沿った体験をユーザに提供できます。

Toaster の作り方例は以前書いた記事があるので参考にどうぞ。
https://www.wantedly.com/users/26190108/post_articles/345447

References

この記事は自分の経験と、以下の本と記事を参考に書きました。

https://www.amazon.co.jp/dp/4802512481
https://www.wantedly.com/companies/wantedly/post_articles/512401

株式会社スタメン

Discussion