コンポーネント設計が先、JSONハーネスは最後 ― UIの仕様を「上書き禁止」で閉じ込める
Published on 2026年5月3日
設計はじめに
最近、DESIGN.md + contracts(JSON) + harness のような構成で「UI仕様や禁止パターンを JSON に集約し、AI やツールから機械的に検証できるようにする」という話を見かける機会が増えた。Markdown とコードのあいだのドリフトを防ぎ、Tailwind クラス単位の禁止ルールを CI で落とせるようにする、という趣旨だ。
仕組みとしては理解できる。けれど、自分の中ではどうしても順序が引っかかる。
そもそも、UIコンポーネント設計とその運用ルールがきちんと成立していれば、JSON ベースの禁止ルールやハーネスは「最後に足す保険」でしかないのではないか。
これが今回の出発点だ。本記事では、自分が「コンポーネント主義を最優先にする」立場を取る理由と、ハーネスや JSON ルールがどの位置に来るべきかを整理する。
過去に書いた Atomic Design を用いたコンポーネントの実装について、なぜ Atomic Design を採用するのか、shadcn/ui をどう扱うか と地続きの話でもある。
前提:UIの仕様は「コンポーネント+Storybook+型」で閉じる
自分が理想と置いている順序はシンプルだ。
- UIコンポーネントをデザインシステムに沿って最小単位まで実装する
- Storybook で各コンポーネントのバリアント・状態・レイアウトを整理し、それを事実上の仕様として扱う
- プロダクト実装は、このコンポーネント群の組み合わせだけで完結させる
この前提が守られているなら、Tailwind クラス単位で禁止パターンを JSON 管理したり、ハーネスで検証したりする必要性はかなり薄くなる。仕様は「コンポーネント定義 + Storybook + 型」に閉じ込められているべきで、HTML や class 名レベルでごちゃごちゃ制御しなくても済む世界を目指したい。
| 仕様の置き場所 | 役割 |
|---|---|
| コンポーネント実装 | 振る舞いとスタイルの単一の真実 |
cva などのバリアント |
取りうる状態を網羅し、それ以外を許さない |
| Storybook | バリアント・状態・レイアウトを目視で確認できる仕様書 |
| Props の型 | 「正しい使い方」をコンパイラが強制 |
この四つが揃っていれば、「Markdown と実装のドリフト」も「class 名の野良上書き」も、そもそも発生しにくい構造になる。
「細かな調整が発生すること自体がおかしい」という前提
ここが自分の考えの中核になる。
UIの最小単位コンポーネントの内部でデザインが完結しているなら、「ちょっとだけ padding を足したい」「ここだけ shadow-lg にしたい」といった局所的なデザイン調整が発生すること自体がおかしい。
もしそういうニーズが出てくるなら、それは次のどちらかのシグナルだ。
- コンポーネント設計の漏れ — そのバリアントが本来必要だったのに用意されていない
- レイアウト用コンポーネントの不足 — 余白や配置を担う構造コンポーネントが足りていない
どちらの場合でも、画面側でその場しのぎの上書きを認めてしまうのは設計崩壊への入口だと考えている。「便利だから」「一回だけだから」で className="!pt-3 !shadow-lg" を書いた瞬間、コンポーネントの仕様は実装そのものではなく 「どこかの画面で上書きされている可能性」 にも分散する。仕様の置き場所が壊れたら、もうどんなドキュメントも JSON も追いつかない。
自分が取る運用ルール
この前提に立つと、自然に運用ルールはこうなる。
1. 画面は「設計済みコンポーネントの組み合わせ」だけで構成する
画面(pages / organisms 寄り)には、独自のスタイル定義を置かない。<Card>, <Stack>, <Heading> のような部品を組むだけで完結する状態を保つ。
2. className / style によるスタイル上書きを原則禁止する
これは Atomic Design 記事でも書いた通りだが、外部からスタイルを注入できる口を開けると、コンポーネントの責務がぼやける。「呼び出し元 → 参照するコンポーネント」の依存方向を保ち、参照されるコンポーネントは呼び出し元の事情を知らない状態を維持する。
例外として「呼び出し元から空間配置(margin / 配置)だけは渡せる」設計を取るとしても、それは structure コンポーネントの責務 に閉じ込めるべきで、shadow や padding の上書きとは別物として扱う。
3. 構造・余白も「レイアウト用コンポーネント」として部品化する
余白の入れ方、要素の並べ方、サイズの噛み合わせ——目に見えにくいルールほど、コードに散らばると暗黙知になる。<Stack gap="md"> <Inline align="center"> のように、構造を表すコンポーネントとして部品化しておくと、「方向性」のレベルでも上書きが不要になる。
4. 「ここだけ特別扱いしたい」は新しいバリアントに昇格させる
その場限りのスタイル上書きを認めず、必ず コンポーネントの設計に戻す。size="lg" が必要なら size のバリアントに追加する。エッジケースが頻出するなら、それは別コンポーネントとして切り出すべきというサインだ。
この運用が徹底されているなら、「ちょっとだけ padding を足したい」という欲求が生じた瞬間に、それが設計上の課題として検知できる。そこをルール違反として JSON で検出する必要性は、本来かなり低い。
ハーネスや JSON ルールの位置づけ
ここまで書いた前提の上で、改めて「contracts(JSON) + harness」のような仕組みをどう位置づけるか。
これらが解決しようとしている問題は、おおむね次の四つだ。
| 課題 | 何が起きているか |
|---|---|
| Markdown とコードの数値ドリフト | DESIGN.md と実装の数値が静かにズレる |
| 禁止ルールの分散 | Markdown と Lint と実装の三箇所に同じルールが書かれる |
| 三箇所同期の運用負荷 | どこか 1 つ忘れると整合性が崩れる |
| Claude 前提の記述で他 AI が扱いにくい | 自然言語ベースのルールは AI 依存になりやすい |
これらは確かに現実の問題だ。ただ、よく見るとどれも 「仕様が複数の場所に分散している」状態を前提にした問題 であることに気づく。
仕様の分散を前提に、それを集約する保険を CI に仕込む。
それが contracts(JSON) + harness のアプローチだ。一方、自分が理想とするのは そもそも仕様を分散させない 状態を作ることで、目指している地点が一段違う。
順序:コンポーネント主義 → Lint → ハーネス
優先度を整理するとこうなる。
理想の地点
┌─ ① UIコンポーネントの最小単位でデザインが完結している
│ ② 画面実装はコンポーネントを組み合わせるだけで済む
│ ③ スタイルの直書き・上書きは禁止し、Lintや型でロックする
│ ④ 仕様はStorybookとコンポーネント定義そのものが担う
│
▼
現実の制約が混ざってきたら
┌─ 既存の負債コードが残っている
│ チームや外部コントリビューターの実装揺れが避けられない
│ AI が HTML や Tailwind を直接生成してしまう
│
▼
最後の安全柵として
┌─ ⑤ Tailwind クラス単位の禁止ルール JSON
│ ⑥ Markdown ↔ 実装のドリフト検出
│ ⑦ CI ハーネスでの自動検証
①〜④ が先で、⑤〜⑦ は後だ。順序を逆にすると、ハーネスは「壊れた前提を守るための監視装置」になって、本来直すべきコンポーネント設計の問題から目を逸らす方向に働く。
| 層 | 何を守るか | 失敗したときの影響 |
|---|---|---|
| ① コンポーネント設計 | 仕様そのもの | UI全体の一貫性が崩れる |
| ② 運用ルール(上書き禁止) | 仕様の単一性 | 仕様が画面ごとに分散する |
| ③ Lint / 型 | 機械的に検出できる違反 | コミット時に取りこぼす |
| ④ JSON ハーネス | ①〜③ をすり抜けたケース | CI で最後に止める |
この階層で見ると、ハーネスは 「上の層が漏らしたものを拾う最後の網」 であって、「上の層を構築するためのもの」 ではない。網を先に張っても、上の層が穴だらけならどのみち漏れる。
ハーネスを「便利だから」入れない理由
ここで一つ正直に書いておくと、ハーネスや JSON ルールは入れたくなる気持ち自体は分かる。AI に仕様を読ませやすい、CI で自動検出できる、Markdown と乖離しない——どれも魅力的だ。
それでも自分が「最後にする」と決めているのは、入れた瞬間に 「これがあるから多少の上書きはしてもいい」という心理的な逃げ道 ができるからだ。仕組みは入れた人の意図を超えて運用される。「禁止ルールを書けば守られる」のではなく、「ルールを書いた以外のことは許される」ように受け取られる場面のほうが、現実には多い。
仕組みは、運用ルールが先にあって初めて意味を持つ。仕組みを先に入れると、運用ルールはなし崩しに緩む。
これは テスト戦略 のときに書いた「テストは設計の代わりにならない」と同じ構造だ。テストを増やしても設計の崩壊は止められないように、ハーネスを増やしてもコンポーネント設計の崩壊は止められない。
いま守ること / 後で足してもいいこと
最後に、自分の中の優先度をもう一度シンプルに置き直しておく。
いま守ること(最優先)
- UIコンポーネントを最小単位までデザインシステムに沿って実装する
- 画面はコンポーネントの組み合わせだけで構成する
className/styleの上書きを原則禁止し、必要ならバリアントに昇格させる- 余白・配置もレイアウト用コンポーネントに閉じ込める
- Storybook と型を「仕様の置き場所」として運用する
後で足してもいいこと(最後の保険)
- Tailwind クラス単位の禁止ルール JSON
- Markdown ↔ 実装のドリフト検出
- CI ハーネスでの自動検証
- AI 向けの contracts(JSON) 集約
順序が大事だ。前者が成立していない状態で後者を入れると、構造の腐敗を覆い隠す装置になる。 前者が成立した上で後者を足すなら、それは現実的な制約に対する穏当な保険として機能する。
まとめ
自分の立場をひと言にすると、こうなる。
UI設計の本質はコンポーネント主義にある。JSON ハーネスは、それが守れない現実に対する最後の保険であって、最初に置く土台ではない。
なぜ Atomic Design を採用するのか で書いたとおり、UIコンポーネントの責務は「どう見え、どう操作できるか」だけだ。仕様もこの範囲に閉じ込められる。閉じ込められているなら、外側から「class 名の野良上書きを禁止する JSON」を被せる必要はない。
フレームワーク戦略 で書いた「土台と上物を分ける」と同じ構図でもある。コンポーネント設計は土台、ハーネスは上物。土台が固まる前に上物を積むと、土台ごと傾く。
ハーネスを否定したいわけではない。むしろ、本当に必要になった瞬間に正しく機能させるためにこそ、まずコンポーネント設計を最優先で固める べきだ、というのが結論になる。
順番を間違えなければ、ハーネスは静かに、しかし確実に役立つ最後の網になる。