不動産売買プロダクトのフロントエンド環境を"ルールごと"整備した話 ── UIライブラリ選定とスタイリング設計

はじめに

こんにちは!イタンジ株式会社「ITANDI 売買支援」開発チームの鶴田です。

この記事では、売買プロダクトの新規ページ開発にあたり、UIライブラリの選定からスタイリングルールの策定、さらにそのルールをLinterやAIコーディング支援ツール向けのルール定義で守れるようにした取り組みを紹介します。

技術選定をして終わりではなく、ライブラリの思想を理解し、チームの運用まで含めて設計することを意識しました。

対象読者:

  • フロントエンドのUI基盤をこれから整備しようとしているエンジニア
  • デザインシステムとの接続を意識したライブラリ選定に興味がある方
  • AI時代のコーディングルールの伝え方を模索している方

背景と課題

ITANDI 売買支援は不動産売買仲介業者向けのプロダクトです。既存ページはMUI(旧Material-UI)で実装されていますが、新規ページの開発にあたりUI基盤を見直す機会がありました。

求めていたものは3つです。

  1. 一貫性 — デザイナーが意図した見た目を再現しやすく、開発者ごとのブレが出にくいこと
  2. 保守性 — マジックナンバーやハードコードが散在せず、変更に強いこと
  3. 将来の移行性 — 社内Frontendチームが開発するデザインシステム「ITANDI BB UI」へ段階的に移行しやすいこと

UIライブラリ選定

検討候補

今回の選定では、既存ページで利用しているMaterial-UI(MUI)を継続する案と、新規ページ向けにRadix UI Themesを採用する案を主な候補として検討しました。

Tailwind CSSについても、既存のMUI環境で一部利用されていたため「なぜ使わないのか」を整理する必要はありました。ただし、後述するRadix UI Themesの思想とは相性がよくないと考えていたため、今回のUI基盤の候補には最初から含めていません。

なぜRadix UI Themesか

選定にあたって重視した軸は以下の3つです。

1つ目は、Closed Component System(閉じたコンポーネント設計)です。

Radix UI Themesの公式ドキュメントには、以下のように書かれています。

The components in Radix Themes are relatively closed—they come with a set of styles that aren't always easily overridden. They are customizable within what's allowed by their props and the theme configuration.

コンポーネントのスタイルが閉じていることは、一見すると不自由に思えます。しかし、開発者が個別にスタイルを上書きする余地が少ない分、チーム全体でデザインの一貫性を保ちやすくなります。

「propsとtheme configurationで制御する」という制約は、デザイナーとエンジニアの共通言語にもなります。

2つ目は、デザイントークンとしてのCSS変数です。

Radix UI Themesは、コンポーネントを構成するCSS変数(デザイントークン)を公開しています。var(--space-4)var(--blue-9) といったトークンを使ってカスタムコンポーネントを作れば、テーマの変更が自動的に波及します。

3つ目は、ITANDI BB UIへの移行を見据えたトークン互換性です。

イタンジにはFrontendチームが開発・メンテナンスする社内デザインシステム「ITANDI BB UI」が存在します。少なくとも2024年2月時点で133以上のコンポーネントが提供されており、賃貸系プロダクトで広く利用されています。

ITANDI BB UIのデザイントークン設計はRadix UI Themesのトークン体系をベースとしているため、売買プロダクトでもRadix UI Themesのトークンに沿って実装しておけば、将来的にITANDI BB UIへ移行する際のコストを下げられます。

なぜTailwindを採用しなかったか

Tailwind CSSは便利なツールですが、Radix UI Themesとの併用には構造的な不整合があります。公式ドキュメントでも以下のように説明されています。

Tailwind is a different styling paradigm, which may not mix well with the idea of a closed component system where customization is achieved through props, tokens, and creating new components on top of a shared set of building blocks.

Radix UI Themesは「propsとトークンでカスタマイズする」思想です。一方で、Tailwindは「ユーティリティクラスを組み合わせてスタイルを作る」思想です。この2つはスタイリングの考え方が大きく異なります。

また、Tailwindの @tailwind baseRadix Themesのボタンスタイルを上書きするなど、技術的な干渉も報告されています。思想と実装の両面から、併用は避ける判断をしました。

スタイリングルール策定

基本方針: ライブラリの思想に乗る

Radix UI Themesを使う以上、中途半端にカスタマイズするのではなく、その思想に沿って実装することを方針にしました。

具体的には、以下の優先順位でスタイリングを行います。

1. Radix UI Themes標準コンポーネント + props(最優先)
2. Radix Primitives + Themesコンポーネント(Box, Flex)でラップ
3. インラインスタイル + Radix UIのCSS変数
4. globals.css(複雑なセレクタ、アニメーション等)
5. CSS Modulesは使用しない(スタイルの所在が分散する)
6. CSS-in-JSで独自のスタイルレイヤーを作らない

例えば、赤いボタンを作りたいときは、カスタムスタイルで無理に色を指定するのではなく、Radix UI Themesの標準propsを使います。

// 避けたい例
<Button style={{ backgroundColor: 'var(--red-9)', color: 'white' }}>
  削除
</Button>

// 推奨する例
<Button color="red" variant="solid">
  削除
</Button>

この方針が特に表れる具体例として、z-indexの扱いがあります。

Radix UI Themesは、zIndex: 1000 のような数値で重なり順を管理するのではなく、重なりが必要なUIはPortalで描画する考え方を推奨しています。公式ドキュメントでも、z-indexauto, 0, -1 などの限定的な値にとどめ、重ねる必要がある要素はPortalで描画する方針が示されています。

そのため、売買プロダクトでも z-index の数値指定は原則避けるルールにしました。単に禁止事項を増やしたかったわけではなく、重なり順の責務を各実装者の判断に置かず、Radix UI Themesの仕組みに寄せたかったためです。

運用・マインドセットの設計

技術的なルールだけでなく、チームの判断基準も明文化しました。

特に重要なのは、Radix UI Themesに存在しないUIコンポーネントを独自に作成したくなったときの運用です。

  1. まず「Radix UI Themesの標準機能で実現できないか?」を検討する
  2. 実現できない場合、デザイナーに相談し、本当にそのUIが必要か確認する
  3. 必要な場合は、Radix Primitives + Themesのレイアウトコンポーネント(Box, Flex)でラップして実装する

この「デザイナーに確認する」ステップを入れているのは、せっかくUIライブラリを使用しているのに独自UIコンポーネントの保守工数が増えてしまうことを防ぐためです。 実装で無理に解決する前に、そもそもの設計を見直すきっかけにしています。

デザイントークンガイドの整備

どの値をデザイントークン(CSS変数)で指定し、どの値をコンポーネント固有の値として扱ってよいかの判断基準を整理しました。

プロパティ 扱い
padding, margin, gap トークンを使う var(--space-4)
fontSize トークンを使う var(--font-size-2)
color, backgroundColor トークンを使う var(--blue-9)
top, right, left, bottom コンポーネント固有の値も許容 '4px'(視覚的微調整)
const NOTIFICATION_DOT_OFFSET = '4px'

<div style={{
  padding: 'var(--space-4)',
  position: 'absolute',
  top: NOTIFICATION_DOT_OFFSET,
}}>

余白や色はプロダクト全体の一貫性に関わるためトークンを使います。一方で、絶対配置の微調整のようにコンポーネント固有になりやすい値は、定数化したうえで許容する方針にしました。

仕組みで守る: LinterとAIコーディング支援ツール向けルール

ルールを作っても、人間のレビューだけで守り続けるのは難しいです。そこで、LinterとAIコーディング支援ツール向けのルール定義を整備しました。

カスタムESLintルール: no-hardcoded-design-values

デザイントークンを使用すべき箇所でハードコードを行うとESLintエラーになるカスタムルールを作りました。

検出対象は以下のような値です。

カテゴリ NG例 推奨
'#004487', 'rgb(0,68,135)' 'var(--blue-9)'
余白 '8px', '1rem' 'var(--space-2)'
フォントサイズ '14px' 'var(--font-size-2)'
ボーダーラジアス '4px' 'var(--radius-1)'
z-index 1000, 100 auto, 0, -1 のみ許可

単純なリテラル値だけでなく、三項演算子や論理演算子の中にあるハードコード値も検出します。

// 三項演算子内でも検出される
const style = {
  color: isError ? '#ff0000' : '#000000',
}

// 変数を経由しても再帰的に追跡して検出される
const errorColor = '#ff0000'
const style = {
  color: errorColor,
}

変数参照も(少なくとも同一ファイル内では)定義元まで追跡することで、「定数に切り出せば検出されない」という抜け道を減らしています。

AIコーディング支援ツール向けルール定義

AIコーディング支援ツールがコードを生成・編集する際にも、プロジェクトのルールを参照できるようにしました。

ルール定義は、以下のように責務を分けて管理しています。

- 全般的なルール
- スタイリングルール
- コンポーネント配置ルール
- レイアウトルール

例えばスタイリングルールには、以下のような内容を記載しています。

## 基本方針
- Radix UI Themesの標準機能を最優先
- CSS変数を使用する

## スタイリング手法の優先順位
1. Radix UI Themes標準コンポーネント + props
2. Primitives + Themesコンポーネント(Box、Flex)でラップ + インラインスタイル
3. インラインスタイル + Radix UIのCSS変数
4. globals.css(複雑なセレクタ、アニメーション、メディアクエリなど)

これにより、AIコーディング支援ツールが CSS Modules を使ったコードを生成したり、padding: '16px' のようなハードコードを書いたりすることが減りました。

ルール定義には「何をするか」だけでなく、「なぜそうするか」も書くようにしています。背景が書かれている方が、AIコーディング支援ツールも文脈に沿ったコードを生成しやすくなるためです。

3層で守る構造

最終的には、以下の3層でルールを守る形にしました。

┌─────────────────────────────────────────┐
│  ① AIコーディング支援ツール                │ ← 生成・編集時
├─────────────────────────────────────────┤
│  ② Linter(no-hardcoded-design-values) │ ← 機械チェック
├─────────────────────────────────────────┤
│  ③ 人間(コードレビュー)                  │ ← 意図・設計判断
└─────────────────────────────────────────┘

AIコーディング支援ツールが最初からルールに沿ったコードを生成・編集し、Linterが機械的にチェックし、人間は設計判断に集中する。この役割分担にすることで、レビューの負荷を下げつつ品質を保てるようにしています。

効果と振り返り

この取り組みにより、以下の効果がありました。

  • デザインの一貫性: マジックナンバーが減り、各ページで同じトークンを使いやすくなった
  • レビュー効率の向上: スタイリングに関する機械的な指摘をLinterに任せられるようになった
  • AI活用の促進: AIコーディング支援ツールがプロジェクトのルールを踏まえたコードを生成しやすくなった
  • オンボーディング: 新しいメンバーが参加したときに、判断基準を共有しやすくなった

一方で、「Radix UI Themesに存在しないコンポーネントをどう作るか」の判断は、まだデザイナーとの都度相談に依存しています。この部分のガイドラインは、今後さらに整備していきたいです。

おわりに

UIライブラリの選定は「何を使うか」で終わりがちですが、実際にはそのライブラリの思想を理解し、チーム全体がその思想に沿って開発できる環境を整えることが大切だと感じました。

Radix UI ThemesのClosed Component Systemという設計思想を受け入れ、propsとトークンでカスタマイズする方針を徹底する。そのために、ガイドライン、Linter、AIコーディング支援ツール向けルール定義を整えました。

この一連の取り組みが、チームの開発体験と成果物の品質につながっていると感じています。

最後までお読みいただきありがとうございました!