🐙

AI Agent時代の社内UIライブラリの作り方

に公開

この記事は、Finatext Advent Calendar 2025 の 14 日目の記事です。

こんにちは!Finatextのクレジットドメインでエンジニアをしている名澤です。
今回は弊社で開発中のAIネイティブな社内UIライブラリ「kinako-ui」について紹介します!

ドキュメントの画面・Selectコンポーネント

前提

クレジットドメインでは「Crest」という貸金プラットフォームを提供しているのですが、合わせて顧客向けのアプリケーションの構築も行っています。

ただこのアプリケーションに関してはほとんどドキュメントが制定されていなかったため、以下のような課題が出てきました。

  • 不適切な粒度のコンポーネントが作られる
  • 機能が重複しているコンポーネントが作られる
  • 不用意なpropsの追加
  • カラー/余白等のデザインルールが守られていない

そこでこの問題を解決すべく、社内向けのUIライブラリを作ることにしました。

コンセプト

デザインシステムからズレない設計

propsで一切のstyleを受け取らず、セマンティックな値のみを受け取ります。

// ❌ 避けたいパターン:styleやclassNameを直接受け取る
<Button className="bg-blue-500 px-4" style={{ margin: '8px' }}>送信</Button>

// ✅ kinako-uiのパターン:セマンティックな値のみ
<Button variant="primary" size="md">送信</Button>

ここに関しては、以前書いたテックブログで、Tailwindをできるだけ負債にしないコンポーネントの実装方針として紹介しているので、合わせてご覧ください。

https://zenn.dev/finatext/articles/8cb614fd7fd3b5

人にもAIにも読みやすいドキュメント/UIカタログ

今回はよく使われるStorybookを使った方式ではなく、MDX + FlexSearchを使ってドキュメント/UIカタログを作りました。

Storybookを使うメリットとして、Control panelsでpropsが操作できたり、Story自体がテストとして動作する点がありますが、今回はこれらのメリットよりも以下を優先しました。

  • 一覧性・検索性の高さ:必要なコンポーネントにすぐたどり着ける
  • ビルド時間の短縮:シンプルなMDXベースで高速ビルド
  • AIフレンドリー:Markdownベースなのでコンテキストとして渡しやすい

特にAIによってドキュメントの自動生成がかなり簡単になったため、ドキュメント構築/最新化のコストがぐんと下がったのもこの方式を採用した一因です。

ライブラリ単位でカスタム可能なUI

kinako-uiは複数のクライアントアプリケーション開発で使うライブラリのため、完全に特定のプロダクトに紐づいたデザインシステムを採用することはできません。

そのため一定のルールを設けて、カラーや角丸のようなプロパティをカスタムできる必要があります。kinako-uiでは、CSS変数を使ってこれを実現しています。

/* Default CSS */
@import "@Finatext/kinako-ui/styles.css";

:root {
  /* Customizable Colors */
  /* Primary */
  --color-primary: oklch(93.1% 0.026 92);
  --color-primary-alternative: oklch(78.7% 0.044 91);
  --color-on-primary: var(--color-black);

  /* Secondary */
  --color-secondary: oklch(46.8% 0 263);
  --color-secondary-alternative: oklch(36.0% 0 263);
  --color-on-secondary: var(--color-white);

  /* Accent */
  --color-accent: oklch(78.6% 0.131 170);
  --color-accent-alternative: oklch(69.9% 0.134 165);
  --color-on-accent: var(--color-white);

  /* Semantic Properties */
  /* Base Values */
  --radius-base: 0.5rem;
  --radius-lg: 1.5rem;

  /* Container Width */
  --container-sm: 428px;
  --container-md: 768px;
  --container-lg: 928px;
}

カラーにはoklch色空間を採用しました。oklchはLightness(明度)、Chroma(彩度)、Hue(色相)で色を表現するため、人間の知覚に近い形で色を調整できます。primary-alternativeのような派生色を作る際にも、明度だけを変えれば自然な濃淡が作れるのが利点です。

参考:
https://developer.mozilla.org/ja/docs/Web/CSS/Reference/Values/color_value/oklch
https://qiita.com/soi/items/9439ba59cef99b1a1ea5

Theme Generator

CSS変数を手で調整するのは大変なので、GUIでテーマをカスタマイズできるTheme Generatorも用意しています。

カスタムカラーを選択して、プレビューを見ながら調整できます。最終的にはCSSファイルとしてエクスポートできるので、そのままプロジェクトに取り込めます。

技術スタック

Vite + React

今回は社内向けドキュメントということもあり、パフォーマンスはそこまで求めていなかったのでシンプルなVite + ReactのSPA構成にしました。

ただ、一覧や検索などのパフォーマンスが、コンポーネントが増えるほど悪くなっていってしまう構成なので、どこかでVikeなどを使ってSSGへ移行することも検討しています。

Tailwind

元々はstyled-componentsやCSS Moduleなどを使うことも検討していましたが、以下の理由からTailwindを採用しました。

  • 元々顧客アプリにはTailwindを使っている
  • 1ファイルでUI構築を完結できてAIと相性が良い
  • CSS-in-JSのランタイムコストを避けられる
  • デザイントークンとの親和性が高い

Radix UI

shadcn/uiで使われていることもあり、TailwindやAIとの相性も良いヘッドレスUIライブラリであるRadixを採用しました。

AdobeのReact Ariaも検討しましたが、以前別のアプリケーションで採用した際、情報が少なく、特にステート管理が複雑なコンポーネントを実装する際に迷ってしまったことがあったため、今回は採用を見送りました。

MDX

MarkdownにReactを書けるMDXを使うことで、AIが読みやすいMarkdownファイルと、開発者に見やすいインタラクティブなReact要素の2つを1ファイルに共存させることで、AIネイティブなドキュメントにしています。

# Button

ボタンコンポーネントです。

## 使用例

<ComponentPreview>
  <Button variant="primary">送信する</Button>
</ComponentPreview>

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'primary' \| 'secondary' | 'primary' | ボタンの見た目 |
| size | 'sm' \| 'md' \| 'lg' | 'md' | ボタンのサイズ |

参考:
https://zenn.dev/coji/scraps/5196b48c0ccedb
https://zenn.dev/spring_raining/articles/3eb62ff93df1eb

FlexSearch

algoliaやElasticSearchのようなSaaS形式の全文検索エンジンは今回の要件にはオーバースペックなため除外しました。

その上で候補に上がったのが、Pagefind、Fuse.js、FlexSearchでした。

ライブラリ 特徴 採用判断
Pagefind 静的サイト向け、ビルド時インデックス SSG前提のため除外
Fuse.js あいまい検索に強い 現状では不要な機能
FlexSearch 高速、メモリ効率が良い 採用

FlexSearchはデフォルトだと形態素解析を行わないため、特にTypoや日本語のあいまい検索には向かずそういった用途にはFuse.jsが採用されることが多いと思いますが、現状は「ButtonはButton」「CardはCard」のように英語のコンポーネント名で検索することが多いため大きな問題にはなっていませんが、日本語での説明文検索を強化したい場合は、将来的に形態素解析ライブラリとの組み合わせや、Fuse.jsへの移行が必要になるかもしれません。

実際に検索しているところ

参考:
https://qiita.com/Sashimimochi/items/4972b3dc333c6e5fb866
https://azukiazusa.dev/blog/static-site-search-engine-and-ui-library-pagefind/

AIエージェントで開発を効率化するための工夫

ドキュメント生成用の指示

ドキュメント生成用にmarkdownで指示を用意しているので、コンポーネントのパスとこの指示を読ませてあげることで、ほぼ自動でMDXドキュメントが生成できます。

プロンプト全文

コンポーネントドキュメント作成チュートリアル

このドキュメントは、kinako-UIのコンポーネントドキュメントを作成するためのガイドラインです。Button.mdxの実装例を参考に、他のコンポーネントのドキュメントを一貫性のある形式で作成できるようになります。

📁 ディレクトリ構造

src/docs/content/
├── index.mdx                 # ドキュメントのホームページ
└── [component-name]/          # コンポーネントごとのディレクトリ
    ├── [component-name].mdx  # メインドキュメント
    └── examples/              # サンプルコードディレクトリ
        ├── BasicExample.tsx   # 基本的な使用例
        ├── VariantExample.tsx # バリエーション例
        └── ...                # その他の使用例

📝 MDXファイルの基本構造

コンポーネントのMDXファイル(例:src/docs/content/[component-name]/[component-name].mdx)は以下の構造に従います:

1. インポート文(必須)

import { BasicExample } from './examples/BasicExample'
import { VariantExample } from './examples/VariantExample'
// 他のサンプルコンポーネントをインポート

2. タイトルと説明(必須)

# [コンポーネント名]

[コンポーネントの簡潔な説明を1-2文で記載]

3. Usage セクション(必須)

基本的な使い方を示すコードスニペット:

## Usage

```tsx
import { [ComponentName] } from '@/components/lv1/[ComponentName]'

export default function Example() {
	return <[ComponentName]>基本的な使用例</[ComponentName]>
}

### 4. Examples セクション(必須)

実際のサンプルコンポーネントを表示:

```mdx
## Examples

### [例のタイトル]

[この例の簡単な説明]

<ExampleComponent />

5. API Reference セクション(必須)

コンポーネントのプロパティを表形式で記載:

## API Reference

### [ComponentName]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `propName` | `type` | `default` | プロパティの説明 |
| `children` | `ReactNode` | - | コンポーネント内に表示するコンテンツ |

🎨 サンプルコンポーネントの作成

⚠️ 重要: 関連コンポーネントのProps確認

サンプルコンポーネントを作成する際は、必ず使用するすべてのコンポーネントのpropsを確認してください:

  1. メインコンポーネント: ドキュメント化する対象のコンポーネント(例: Select)
  2. レイアウトコンポーネント: Stack などのレイアウト用コンポーネント
    • spacing vs gap - どちらのプロパティが正しいか確認
    • direction, justifyContent, alignItems などの有効な値を確認
  3. ユーティリティコンポーネント: ComponentPreview などのドキュメント専用コンポーネント
  4. 型定義: SelectOption のような型定義は正しくインポート(type キーワードを使用)

:

// ✅ 正しい: 型のインポートには type を使用
import { Select, type SelectOption } from '@/components/lv1/Select'

// ✅ 正しい: Stackコンポーネントの実際のpropsを確認して使用
<Stack direction="horizontal" gap={2}> // spacingではなくgapが正しいプロパティ

// ❌ 間違い: 存在しないプロパティを使用
<Stack spacing={2}> // Stackコンポーネントにspacingプロパティは存在しない

各サンプルファイル(examples/[ExampleName].tsx)の構造:

import { [ComponentName] } from '@/components/lv1/[ComponentName]'
import { Stack } from '@/components/lv1/Stack' // 必要に応じて
import { ComponentPreview } from '@/docs/components/ComponentPreview'

// 表示するコードを文字列として定義
const code = `
<[ComponentName] prop="value">
	コンテンツ
</[ComponentName]>
`

export function [ExampleName]() {
	return (
		<ComponentPreview code={code}>
			{/* 実際のコンポーネントレンダリング */}
			<Stack direction="horizontal" justifyContent="center" alignItems="center">
				<[ComponentName] prop="value">
					コンテンツ
				</[ComponentName]>
			</Stack>
		</ComponentPreview>
	)
}

✅ ドキュメント作成のチェックリスト

新しいコンポーネントドキュメントを作成する際は、以下の項目を確認してください:

基本構造

  • コンポーネント用のディレクトリを作成(src/docs/content/[component-name]/
  • メインMDXファイルを作成([component-name].mdx
  • examplesディレクトリを作成
  • App.tsxにページを追加(重要!)

MDXファイルの内容

  • 必要なサンプルコンポーネントをインポート
  • コンポーネント名と簡潔な説明を記載
  • Usageセクションで基本的な使用方法を説明
  • Examplesセクションで実際のサンプルを表示
  • API Referenceセクションですべてのプロパティを文書化

サンプルコンポーネント

  • 基本的な使用例(BasicExample)
  • バリエーション例(必要に応じて)
  • エッジケースの例(必要に応じて)
  • ComponentPreviewコンポーネントを使用してコードを表示可能に
  • 使用するすべてのコンポーネント(Stack、ComponentPreviewなど)のpropsを確認
  • 型定義のインポートには type キーワードを使用

API Reference

  • すべてのプロパティを記載
  • 型情報を正確に記載
  • デフォルト値を明記
  • わかりやすい説明を日本語または英語で記載

💡 ベストプラクティス

  1. 一貫性を保つ: Button.mdxの構造とスタイルに従う
  2. 実用的な例を提供: 実際のユースケースに即したサンプルを作成
  3. 段階的に説明: 基本的な使い方から高度な使い方へ順番に
  4. コードの可視化: ComponentPreviewを使ってコードと実行結果を両方表示
  5. 完全性: すべてのプロパティとオプションを文書化
  6. 依存コンポーネントの確認: ドキュメント内で使用するすべてのコンポーネント(Stack、ComponentPreviewなど)のpropsを確認し、正しく使用する

🚀 実装例

Button.mdxの実装を参考例として確認:

  • ファイルパス: src/docs/content/button/button.mdx
  • 5つの異なるサンプルを提供(Variants、Width、Rounded、Link、Disabled)
  • 各サンプルは独立したTSXファイルとして実装
  • API Referenceで全プロパティを詳細に文書化

🔧 App.tsx への追加(重要)

新しいコンポーネントドキュメントを作成したら、必ず src/App.tsx にルーティングを追加する必要があります:

1. MDXページのインポートを追加

// Lazy load MDX pages
const IndexPage = lazy(() => import('./docs/content/index.mdx'))
const ButtonPage = lazy(() => import('./docs/content/button/button.mdx'))
const [ComponentName]Page = lazy(() => import('./docs/content/[component-name]/[component-name].mdx')) // 追加

2. ルーティングにページを追加

<Routes>
	<Route element={<DocLayout />}>
		<Route path="/" element={<MDXProvider components={mdxComponents} />}>
			<Route index element={<IndexPage />} />
			<Route path="components/button" element={<ButtonPage />} />
			<Route path="components/[component-name]" element={<[ComponentName]Page />} /> {/* 追加 */}
		</Route>
	</Route>
</Routes>

注意: この手順を忘れると、ドキュメントサイトでコンポーネントページにアクセスできません!

📋 テンプレート

以下は新しいコンポーネントドキュメント用のテンプレートです:

import { BasicExample } from './examples/BasicExample'
import { AdvancedExample } from './examples/AdvancedExample'

# [ComponentName]

[コンポーネントの説明を1-2文で記載]

## Usage

```tsx
import { [ComponentName] } from '@/components/lv1/[ComponentName]'

export default function Example() {
	return <[ComponentName]>Content</[ComponentName]>
}

Examples

Basic Usage

[基本的な使用例の説明]

<BasicExample />

Advanced Usage

[高度な使用例の説明]

<AdvancedExample />

API Reference

[ComponentName]

Prop Type Default Description
prop1 string "default" プロパティ1の説明
prop2 boolean false プロパティ2の説明
children ReactNode - 子要素

---

またClaude CodeのCustom Slash Commandとして登録することで、/generate-doc src/components/Button.tsxのように呼び出すだけでドキュメントが生成できます。

llms.txt・llms-full.txt

llms.txtはLLM向けに最適化されたドキュメントをコンテキストとして渡すために作られた規格です。

https://llmstxt.org/

通常のWebページをLLMに読ませようとすると、マークアップ、広告、JavaScript等のノイズが大量に含まれてしまいます。llms.txtはそういった余計なものを省いて、必要な情報だけをMarkdownで提供するためのファイルです。

既にAnthropicやVercel、Cloudflareなどの企業が提供しています。

https://docs.claude.com/llms.txt
https://vercel.com/docs/rest-api/reference/llms.txt
https://developers.cloudflare.com/llms.txt

また、/llms.txt directory というllms.txtを提供しているサービスを一覧してくれているサイトもあります。
https://directory.llmstxt.cloud/llms.text

llms.txtの他にllms-full.txtというファイルもあり、こちらはMarkdown形式でサイト情報を全て記述しているドキュメントです。

Cloudflareの例で見ると390万トークン以上記述されています(Claude Opus 4.5によるとハリーポッター全巻3セット分くらいあるらしい!)

https://developers.cloudflare.com/llms-full.txt

kinako-uiでの実装

kinako-uiでは、これらのファイルは手書きではなく、MDXドキュメントから自動生成しています。

// scripts/generate-llms-txt.ts(簡略版)

// MDXからJSXコンポーネントとimportを除去してプレーンなMarkdownに変換
function cleanMDXContent(content: string) {
  // コードブロック内は保持
  const codeBlocks: string[] = []
  content = content.replace(/```[\s\S]*?```/g, (match) => {
    codeBlocks.push(match)
    return `__CODE_BLOCK_${codeBlocks.length - 1}__`
  })

  // import文とJSXコンポーネントを除去
  content = content
    .replace(/^import\s+.*?$/gm, '')
    .replace(/<[A-Z][a-zA-Z]*[^>]*>[\s\S]*?<\/[A-Z][a-zA-Z]*>/g, '')
    .replace(/<[A-Z][a-zA-Z]*\s*\/>/g, '')

  // コードブロックを復元
  codeBlocks.forEach((block, i) => {
    content = content.replace(`__CODE_BLOCK_${i}__`, block)
  })

  return content.trim()
}

// MDXファイルを収集してllms-full.txtを生成
const mdxFiles = findMDXFiles('src/docs/content')
const output = mdxFiles
  .map(file => cleanMDXContent(readFileSync(file, 'utf-8')))
  .join('\n\n---\n\n')

writeFileSync('public/llms-full.txt', output)

ポイントは、MDXに含まれる<ComponentPreview>などのJSXコンポーネントやimport文を除去し、LLMが読みやすいプレーンなMarkdownに変換している点です。コードブロック内のJSX(使用例)は保持するため、一度退避してから処理しています。

これをビルド時に実行することで、ドキュメントを更新すればllms.txtも自動的に最新化されます。

実際に使ってみて

良かった点

ドキュメントをAIエージェントに渡すコンテキストとして最適化したことで、以下のような効果がありました。

  • コンポーネント生成の精度向上:AIがkinako-uiのコンポーネントを正しく使ったコードを生成してくれる
  • オンボーディングの高速化:新メンバーがAIに質問しながら学習できる
  • ドキュメント更新の習慣化:生成コストが低いため、コンポーネント修正時にドキュメントも同時更新する文化が根付いた

今後の課題

  • ビジュアルリグレッションテスト:Storybookを使わない分、別途仕組みを検討中
  • SSGへの移行:コンポーネント数が増えてきたらパフォーマンス改善のため移行予定
  • デザイナーとの連携強化:Figmaとの同期など、デザイン〜実装のワークフロー改善

まとめ

kinako-uiは「AIネイティブ」をコンセプトに、以下の特徴を持つUIライブラリです。

  • セマンティックなpropsのみを受け取りデザインシステムからのズレを防止
  • MDXベースのドキュメントでAIとの親和性を確保
  • llms.txtによるLLMフレンドリーな情報提供

AIを活用した開発が当たり前になってきた今、「AIが読みやすい」という観点でのドキュメント設計はますます重要になると考えています。この記事が皆さんの参考になれば幸いです!

Finatext Tech Blog

Discussion