こんにちは、開発2部の早瀬です。
自分のチームではフロントエンドにReact(Next.js)を採用しており、MUIをベースにしたデザインシステムを構築して開発を行なっています。
そこで今回はデザインシステムの構築に至った背景や、具体的な取り組みを紹介したいと思います!
背景
現在バイセルではリユースプラットフォームの開発に注力しています。自分はその中の出品管理サービスのチームに所属しており、他にも並行して複数のサービスの開発が進んでいます。
プラットフォームとして開発をしているので、プロダクト間である程度UI・UXを統一する必要があります。ですが、プラットフォーム全体で使用するデザインシステムは今はまだなく、構築したいという話は出ているのですがまだ先になる見込みです。
そこでチーム間で
- 共通のコンポーネントライブラリを使用する
- 最低限満たすべきデザインルールを作成し準拠する
の2つは必ず満たすようにして、その他は各チームに委ねる形を取ることにしました。
上記さえ守られていればその他は基本的にチーム内で自由に開発をして良いので、各チーム内でスピーディーに意思決定を行いつつ、プラットフォーム全体でのUI・UXを統一することができるようになります。
※ 今回紹介するのは自分が所属するチーム内のデザインシステムについてであり、プラットフォーム全体のデザインシステムについてではないです。
MUIとは
MUIとはReact用のコンポーネントライブラリで、マテリアルデザインをベースに開発されています。かなり豊富なコンポーネントが提供されており、デザインや動作のカスタマイズがしやすい点が魅力的です。またFigmaも提供されており、デザインも組みやすいという点なども考慮してMUIを採用することにしました。
デザインシステムを構築する目的
デザインシステムを構築する上でいくつか目的があります。
デザインと実装の同期
デザインツールにFigmaを使用しており、基本的にはFigmaのデザインに合わせて実装をすることになります。その際にデザインシステムが導入されていればスムーズに実装ができます。
また開発を進めていく上でデザインが変更されることは往々にしてあると思います。その際に変更を反映しやすい仕組みがあれば、デザイン変更時の修正コストが減ります。デザイン変更は軽微なものから大規模なものまで様々ですが、ある程度の頻度で発生するものではあると思うので、運用コストが低い状態を保つのは重要です。
属人性の排除
自分のチームでは開発手法にスクラムを採用しており、エンジニア全員が領域を縛らず開発をする体制になっています。そのため全員がフロントエンドの開発に関わる機会がります。その際にデザインシステムが存在することで、実装を特定の個人に依存する状況を避けることができます。
さらにチーム内で共通認識を持て、レビューコストの削減や開発効率の向上にもつながります。
コミュニケーションコストの削減
デザインシステムをもとに仕様を検討できるため、実装できる/できないの議論を最小限に抑えることができます。
具体的な取り組み
デフォルトスタイルのオーバーライド
MUIではcreateTheme
メソッドとThemeProvider
を使用することでデフォルトのテーマやスタイルを簡単にオーバーライドすることができます。
import { ThemeProvider } from '@mui/material'; import { createTheme } from '@mui/material/styles'; export const theme = createTheme({ palette: { primary: { // プライマリーカラーのオーバーライド main: '#ff4400', }, }, components: { // グローバルで適用させるCSS MuiCssBaseline: { styleOverrides: { a: { textDecoration: 'none', }, }, }, // MUIのInputコンポーネントのスタイルのオーバーライド MuiInputBase: { styleOverrides: { root: { // 適用させたいCSS }, }, }, }, }); export const Sample: React.FC = () => { return ( <ThemeProvider theme={theme}> {/* ... */} </ThemeProvider> ); };
Figma上でMUIのコンポーネントはMaster Componentとして定義し、デザインを組んでもらっています。MUIのコンポーネントのデフォルトスタイルを変更したい場合は、Figma上ではMaster Componentに対して変更を適用します。実装ではMaster Componentに合わせて、createTheme
内でCSSを定義します。こうすることで最小単位のコンポーネントであるMUIのコンポーネントのデザインが同期され、それらを組み合わせて作る画面も自然とデザインに沿った形になるようになっています。
カラーコードの集約
プロダクト内で使用するカラーコードはFigma上で一箇所に集約した上で、各カラーコードに名前を付けています。実装ではFigmaで定義したカラーコードはスタイルと同様に、createTheme
内で全て定義します。
import { createTheme } from '@mui/material/styles'; export const theme = createTheme({ palette: { primary: { main: '#0077C7', extraDark: '#003050', dark: '#00538B', light: '#50B4F6', extraLight: '#EBF4FB', alpha08: '#0077C714', }, noticeRed: { main: '#E01E5A', dark: '#621B16', light: '#FEECEB', }, }, });
使用時には少し冗長なのですがtheme
をimportして使うようにしています。これによりカラーコードが氾濫することなく、型安全に使用することができます。型安全性を考慮しない場合はpalette以下を文字列として指定することもできます。
import { Box } from '@mui/material'; import { theme } from '~/styles/theme'; export const Sample: React.FC = () => { return ( <> <Box sx={{ color: theme.palette.primary.main }} /> <Box sx={{ color: 'primary.main' }} /> </> ); };
またinterfaceの拡張も行うことで、MUIのButtonコンポーネントのcolorとしても受け取れるようになります。
// interfaceの拡張 declare module '@mui/material/Button' { interface ButtonPropsColorOverrides { noticeRed: true; } } // noticeRedが使用可能 <Button color="noticeRed"></Button>
Typographyの定義
fontSizeやfontWeightは使用するものを厳選して使用しています。
実装でも上記の定義に沿ったものしか使用できないように、sizeとweightをconst assertionで定義し、それらをPropsで受け取るTypographyコンポーネントを作成して使用しています。またMUIのTypographyはeslintでimportを禁止して、独自定義のTypographyしか使用できないようにしています。これにより実装上でも予期しないfontSizeやfontWeightが使用されることがなくなります。
const FontSize = { ExtraLarge: 'extraLarge', Large: 'large', Medium: 'medium', Small: 'small', ExtraSmall: 'extraSmall', } as const; export type FontSize = typeof FontSize[keyof typeof FontSize]; const FontSizeStyle: Record<FontSize, number> = { extraLarge: 24, large: 20, medium: 16, small: 14, extraSmall: 12, }; const FontWeight = { Regular: 'regular', Bold: 'bold', } as const; export type FontWeight = typeof FontWeight[keyof typeof FontWeight]; const FontWeightStyle: Record<FontWeight, number> = { regular: 400, bold: 700, }; type TypographyProps = { size?: FontSize; weight?: FontWeight; children: React.ReactNode; }; export const Typography: React.FC<TypographyProps> = ({ size = 'medium', weight = 'regular', children, }) => { return ( <MuiTypography sx={{ fontSize: FontSizeStyle[size], fontWeight: FontWeightStyle[weight], }} > {children} </MuiTypography> ); };
8倍ルール
8倍ルールとは、コンテンツ幅や余白のpxを8の倍数で定義することでデザインに統一性を持たせることができ、デザインの品質が向上するというルールです。ルールがシンプルなので採用しやすく、実装も楽になるというメリットがあり採用しています。
MUIもデフォルトでこのルールを採用しており、spacingという関数を提供してくれています。paddingやmarginのショートハンドでも同じような挙動になります。
const theme = createTheme(); theme.spacing(2); // `${8 * 2}px` = '16px' <Box sx={{ p: 1, m: 2 }} /> // padding: '8px', margin: '16px'
https://mui.com/material-ui/customization/spacing/
自分のチームではこれを積極的に使用していて、他の要素との兼ね合いでFigma上で8の倍数になっていない場合でも丸め込んで実装して良いという形式をとっています。 e.g. 25pxの場合24pxとして、spacing=3で指定
Atomic Designの不採用
下記のような課題からAtomic Designは不採用にしました。
- Organismsの肥大化する
- コンポーネント設計が難しい
代わりにbulletproof-reactを参考にしてfeatureベースのディレクトリ構成を採用しました。(コンポーネントに関係する部分のみ表示しています)
src ... ├── conopnents アプリケーション全体で使用するコンポーネント └── features └── <featureName> 機能ごとにディレクトリを作成 ├── components feature固有のコンポーネント └── pages ページコンポーネント
このディレクトリ構成を採用することでコンポーネントの分類がシンプルになり、コンポーネントの設計に悩む場面が少なくなります。
- アプリケーション全体で使用する→
src/components
- 特定のfeatureで使用する→
src/features/*/components
まとめ
MUIをベースにしたことによって、0からデザインシステムを構築するよりもかなり少ない工数で構築することができました。また、デザインと実装の同期のためのコストも低く、チーム全員でフロントエンドの開発ができており、導入する目的も達成できています!
今回紹介したもの以外にも、チーム内で相談しながらルールを追加していっています。開発のフェーズやチーム構成によって適切なデザインシステムは変わって来ます。なので構築して終わりではなくサイクルを回しながら、今後も改善を続けて行こうと思います。
また、今回はチーム内で使用しているデザインシステムの紹介でしたが、冒頭でも触れたようにゆくゆくはプラットフォーム全体で使用するデザインシステムの構築もしたいと思っています。そこに向けて、チーム内でデザインシステムを運用しながら課題感をためつつ、技術選定なども進めて行きます!
最後にBuySell Technologiesではエンジニアを募集しています。興味がある方はぜひご応募ください!