StorybookにMDXを導入してみたーデザインシステムへの活用ー

サムネイル

はじめに

こんにちは、Yassanです。

私は現在、社内でデザインシステムの構築を目的とした研究チームに所属しており、日々デザイナーと連携しながら開発や改善に取り組んでいます。これまでの取り組みでは、UIコンポーネントの仕様を共有する手段として主にStorybookを活用し、ドキュメントの整備を進めてきました。しかし、メンバー間で「もっとドキュメントの内容を充実させたい」といった声が上がるようになり、より表現力のある方法を模索する中で、StorybookにMDXを導入するという方針が決まりました。

StorybookにMDXを組み合わせることで、コンポーネントの使用例やインタラクティブなデモをそのままドキュメント上に表示することができるため、実装者だけでなく、デザイナーや非エンジニアのメンバーとも直感的に情報を共有できるようになります。

本記事では、実際にMDXを導入してみて感じたことや、使ってみたからこそ見えてきた利点・工夫点などについて、ご紹介したいと思います。

Storybookとは

Storybook公式サイトトップ
Storybook公式サイト:https://storybook.js.org

Storybookは、ReactやVueなどのフレームワークで作られたUIコンポーネントを単体で表示・確認できる開発支援ツールです。見た目や動きを確認しながら開発できるので、デザイナーやエンジニアが同じイメージを持ちやすくなり、効率よく作ることができます。

Storybookでのコンポーネント管理やUIカタログの作成については、こちらの記事で解説しています。あわせてご覧ください!

デザインとエンジニアリングのシームレスな連携〜UIコンポーネント/UIレビュー〜
Storybookで簡単にエンジニア向けのUIカタログを提供する

MDXとは

「Markdown + JSX」の略で、Markdownの書きやすさと、JSXの柔軟な表現力を組み合わせたドキュメント記述形式です。特にStorybookや技術系ドキュメントの分野でよく使われています。

MDXの特徴

項目内容
Markdownの書き方普通のテキストや見出し、リストなどが直感的に書ける
JSXの利用Reactコンポーネントを直接埋め込める
Storybookとの相性UIコンポーネントの説明・デモ・コードを1つのファイルにまとめられる
インタラクティブなドキュメント静的な説明だけでなく、実際に動作するUIも見せられる

デザインシステムプロジェクトの概要

現在、デザインシステムの構築プロジェクトにおいてStorybookを活用しています。

このプロジェクトは、i3DESIGN社内における統一されたUIライブラリの構築と運用を目的として進められている研究活動の一環です。主なゴールは、複数のプロジェクトにおいて共通のデザイン・コンポーネントを活用することで、UIの一貫性を保ちつつ、開発や設計における非効率を減らすことです。

プロジェクトを通じて、以下のような成果が得られることを期待しています。

品質の強化

Figma上で定義されたデザイン仕様と、実際のフロントエンド実装との間に発生しがちな乖離を最小限に抑えることで、見た目や動作のズレを減らし、ユーザーにとって直感的で高品質なUIを提供することを目指します。

開発効率の向上

各プロジェクトで都度コンポーネントをゼロから作成するのではなく、あらかじめ整備されたデザインシステム上のコンポーネント群を再利用することで、設計・開発・レビューの手間を削減し、チーム全体の作業スピードを底上げすることが狙いです。

デザインシステムプロジェクトの社内勉強会の様子は、こちらの記事でご紹介しています!
【社内勉強会レポ】プロダクト品質向上のカギ!デザインシステム構築への挑戦~第一弾~

MDX導入前後の比較

MDXを活用して具体的にどのような変化があったのか、見ていきましょう。

Before(改善前)

これまではStorybook単体での利用にとどまっており、各コンポーネントの画面にはコードとUIの表示だけが存在していました。

改善前の各コンポーネントにはコードとUIの表示のみ

文章による補足説明がなかったため、利用者は実際にプロパティを変更しながら動作を確認し、自分で使い方を理解する必要がありました。その結果、初見の人にとっては理解に時間がかかる傾向があり、属人的な習得になりやすいという課題がありました。

各コンポーネントに自動的にDocsが生成される

BeforeはChip.stories.tsxファイルのみで表現、autodocs設定により、自動的に上記のようなDocsができます。

Chip.stories.tsx

import type { Meta, StoryObj } from "@storybook/react-vite"

import {

 Chip,

 ChipItemType,

 ChipItemSize,

 ChipItemVariant,

 ChipItemProps

} from "./Chip"

import { useState } from "react"

import { mdiLabelOutline, mdiCloseCircleOutline } from "@mdi/js"

const meta: Meta<typeof Chip> = {

 title: "Component/Chip",

 component: Chip,

 tags: ["autodocs"],

 parameters: {

   layout: "centered",

   docs: {

     description: {

       component: "This is a custom Chip component."

     }

   }

 },

 argTypes: {

   type: {

     control: "radio",

     options: ChipItemType

   },

   chipSize: {

     control: "radio",

     options: ChipItemSize

   },

   variant: {

     control: "radio",

     options: ChipItemVariant,

     description: "inputtedはTextfieldの中で使用する"

   }

 }

}

export default meta

type Story = StoryObj<typeof meta>

const InteractiveChip = (args: ChipItemProps) => {

 const [isSelected, setIsSelected] = useState(false)

 const [isVisible, setIsVisible] = useState(true)

 const handleSelect = () => {

   if (!args.isDisabled && args.variant !== "inputted") {

     setIsSelected(!isSelected)

   }

 }

 const handleDelete = (e: React.MouseEvent) => {

   e.stopPropagation()

   if (!args.isDisabled) {

     setIsVisible(false)

   }

 }

 return (

   <>

     {isVisible && (

       <Chip

         {...args}

         isSelected={isSelected}

         onSelect={handleSelect}

         onDelete={handleDelete}

       />

     )}

   </>

 )

}

export const PrimaryOutline: Story = {

 render: InteractiveChip,

 args: {

   type: "primary-outline",

   variant: "default",

   chipSize: "s",

   label: "label",

   startIcon: {

     path: mdiLabelOutline

   },

   endIcon: {

     path: mdiCloseCircleOutline

   },

   isShowStartIcon: true,

   isShowEndIcon: true,

   isDisabled: false

 }

}

export const Inputted: Story = {

 render: InteractiveChip,

 args: {

   type: "primary-outline",

   variant: "inputted",

   chipSize: "s",

   label: "label",

   startIcon: {

     path: mdiLabelOutline

   },

   endIcon: {

     path: mdiCloseCircleOutline

   },

   isShowStartIcon: true,

   isShowEndIcon: true,

   isDisabled: false

 }

}

After(改善後)

Storybookで作成したStoryをMDXファイルから呼び出す構成に変えたことで、大きく改善されました。

UIのデモに加えて説明文や使用方法、注意点などをMarkdownライクな形式で記述できるようになったため、コンポーネントの意図や使い方を自然な流れで把握できるようになりました。

説明文や使用方法、注意点などがMarkdownライクな形式で記述されている改善後の画面(1)

また、GitHubのソースコードのパスや該当ファイルへのリンクも明記しておくことで、エンジニアが実際のコードを確認したい時に、すぐにコードベースにアクセスできる導線を用意できるようになりました。

説明文や使用方法、注意点などがMarkdownライクな形式で記述されている改善後の画面(2)

さらに、コンポーネント使用時の特別な注意などがあれば、コードコメントではなく文章としてわかりやすく明示できるため、設計ミスの予防やナレッジの共有にもつながる点が大きなメリットです。

説明文や使用方法、注意点などがMarkdownライクな形式で記述されている改善後の画面(2)

Chip.mdxで、storyを呼び出す書き方ができ、文章なども組み合わせてよりリッチな表現ができるようになりました。(※AfterのChip.stories.tsxのstoryは省略しています。)

Chip.mdx

import {

  Canvas,

  Meta,

  Title,

  Subtitle,

  Primary,

  Stories,

  ArgTypes,

  Controls

} from "@storybook/blocks"

import * as ChipStories from "./Chip.stories"

<Meta of={ChipStories} />

# Chip

<Subtitle>

  [Source

  Code](https://github.com/i3design-lab/i3d-react-ui-components/tree/main/src/components/Chip)

</Subtitle>

チップは、情報を視覚的に分類・表示するためのコンポーネントです。タグ、ステータス、カテゴリーなどの表示に使用されます。

## Stories

デザインシステム内のアイコンは主にMDIからインポートされたものを使用しています。

そのため、startIconとendIconのpath指定方法に関しては、Icon/MDIを参照してください。

<Primary />

<Controls />

### Type

3種類のtypeがあります:

- Gray Outline

- Primary Fill

- Primary Outline

<Canvas of={ChipStories.AllTypes} /> // storyを呼び出している

### Size

チップには以下の2つのサイズがあります。

- Small (s)

- Medium (m)

<Canvas of={ChipStories.SizeVariants} /> // storyを呼び出している

### Icon

アイコンの表示パターンは以下の4種類があります。

- アイコンなし

- 開始アイコンのみ

- 終了アイコンのみ

- 開始と終了アイコン両方

<Canvas of={ChipStories.IconVariants} /> // storyを呼び出している

### Disabled

<Canvas of={ChipStories.StateVariants} /> // storyを呼び出している

 さらなる改善に向けて

StorybookへのMDX導入により、コンポーネントの理解がしやすくなり、開発チーム内での情報共有もスムーズになりました。現在は、ここからさらに進んでおり、MCPサーバーの導入なども行っているところです。今後もデザインシステムの研究と改善を継続することで、プロジェクト全体の品質向上や開発効率のさらなる向上につなげていきたいと考えています。