Alexis Mabanza @ alexvolkihar.ovh

Atomic Designを極める:コピペからデザインシステムへ

Jul 28 · 18min

English Version · Version Française

スライド: SPA(フランスのみ)

Slidev作成さくせい - presentation slides for developers.

UIは、アプリケーションのなかもっとざつあつかわれがちなレイヤーだ。納期のうきわれながら「このページだけ」マークアップのブロックを複製ふくせいし、「この場合ばあいだけ」ユーティリティクラスを追加ついかする。半年はんとし、デザインチームからボタンの角丸かどまるえてほしいとわれてはじめてづく。プライマリボタンの実装じっそうが14とおりも存在そんざいし、23のファイルにらばり、微妙びみょうちがあおが7しょくもあることに。

これはアーキテクチャをたないインターフェース症状しょうじょうだ。フレームワークにみつ結合けつごうしたビジネスコードとまったくおな問題もんだいが、プレゼンテーションそうかたちえてあらわれているにすぎない。

ここで登場とうじょうするのがAtomic Designである。Brad Frostが2013ねん提唱ていしょうし、2016ねん同名どうめい著書ちょしょ発展はってんさせたこのモデルは、インターフェースをページの集合しゅうごうとしてではなく、階層かいそうされ、さい利用りよう可能かのうで、単体たんたいでテストできるコンポーネントのシステムとしてとらえることを提案ていあんする。

この記事きじは、そういう種類しゅるいのページから出発しゅっぱつし、モデルの5つのレベルを一通ひととおたうえで、おなじシステムを2かいつくる。1かいはサーバーがわSymfony UX Twig Componentsで、もう1かいはクライアントがわVue 3で。あえて2かいつくるのがポイントだ。このモデルがどちらのフレームワークにも依存いぞんしないことをしめすためである。


1. 出発しゅっぱつてん:コピペでつくられたインターフェース

まずは、ひとかたまりでかれた商品しょうひん一覧いちらんてみよう。とくわったところはない。

{# templates/catalog/list.html.twig #}
<section class="py-8 px-6">
    <h2 style="font-size: 24px; font-weight: 700; color: #1a1a1a; margin-bottom: 24px;">
        Our products
    </h2>

    <div class="grid grid-cols-3 gap-6">
        {% for product in products %}
            <article class="border border-gray-200 rounded-lg p-4 shadow-sm">
                <img src="{{ product.imageUrl }}" alt="{{ product.name }}" class="w-full h-48 object-cover rounded">

                <h3 style="font-size: 18px; font-weight: 600; margin-top: 12px;">
                    {{ product.name }}
                </h3>

                <p style="color: #6b7280; font-size: 14px; margin-top: 4px;">
                    {{ product.description|slice(0, 80) }}
                </p>

                {# Price formatting duplicated across 6 other templates #}
                <p style="font-size: 20px; font-weight: 700; color: #2563eb; margin-top: 8px;">
                    ${{ (product.priceCents / 100)|number_format(2) }}
                </p>

                {% if product.stock > 0 %}
                    <span style="background: #dcfce7; color: #166534; padding: 2px 8px; border-radius: 9999px; font-size: 12px;">
                        In stock
                    </span>
                {% else %}
                    <span style="background: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 9999px; font-size: 12px;">
                        Out of stock
                    </span>
                {% endif %}

                {# The "primary button", hand-written for the 14th time #}
                <button
                    onclick="fetch('/api/cart/add', { method: 'POST', body: JSON.stringify({ id: {{ product.id }} }) }).then(() => location.reload())"
                    style="background: #2563eb; color: white; padding: 8px 16px; border-radius: 6px; border: none; width: 100%; margin-top: 16px; cursor: pointer;"
                    {% if product.stock == 0 %}disabled style="opacity: 0.5"{% endif %}
                >
                    Add to cart
                </button>
            </article>
        {% endfor %}
    </div>
</section>

このテンプレートが脆弱ぜいじゃく理由りゆう

これは一応いちおううごく。グリッドを描画びょうがし、在庫ざいこ状態じょうたいあつかい、カートに追加ついかもできる。本番ほんばん環境かんきょうのこのページをたデザイナーは、とく文句もんくわないだろう。

しかしこれは、5つの独立どくりつした理由りゆうから、複利ふくりふくらんでいく負債ふさいでもある。

1. 唯一ゆいいつ正解せいかいとなる情報じょうほうげんがない

あお#2563eb角丸かどまる6px余白よはく8px 16pxは、こことの13のファイルにべたされている。「プライマリボタン」の定義ていぎがどこにも存在そんざいしない。変更へんこうするには全文ぜんぶん検索けんさく置換ちかんをするしかなく、かならずどこかで見落みおとして、しずかなのずれがまれる。

その結果けっか測定そくてい可能かのうだ。Figmaのモックアップと本番ほんばん乖離かいりはスプリントをかさねるごとにひろがり、やがてだれもどちらも信用しんようしなくなる。

2. 表示ひょうじロジックの重複じゅうふく

価格かかくのフォーマット(priceCents / 100けた区切くぎり、通貨つうか記号きごう)は、価格かかく表示ひょうじされる場所ばしょすべてでかえされている。通貨つうか対応たいおう税込ぜいこみ表示ひょうじ追加ついかするには、すべての出現しゅつげん箇所かしょさが必要ひつようがある。これはビジネスロジックではなく表示ひょうじロジックであり、おなじだけの配慮はいりょあたいする。

3. 単体たんたいでテストも文書ぶんしょもできない描画びょうが

無効むこうされたボタンの確認かくにんするには、アプリケーションを起動きどうし、ログインし、カタログまで移動いどうし、在庫ざいこれの商品しょうひんさが必要ひつようがある。ボタンだけを、その6とおりのバリエーションを、1びょう描画びょうがする方法ほうほう存在そんざいしない。

結果けっかとして、レアケース(エラー、ローディング、非常ひじょうながいテキスト、そらリスト)は本番ほんばん爆発ばくはつするまでだれにもれない。

4. ビジネスロジックと通信つうしんへの視覚しかくコンポーネントの結合けつごう

このボタンは/api/cart/addというURLをっていて、JSONペイロードのかたち把握はあくし、ページをリロードすることまでめている。視覚しかくてきなコンポーネントがネットワークの責務せきむ背負せおってしまっている。このボタンをカートごときずらずに場所ばしょさい利用りようすることは不可能ふかのうだ。

5. デザインと開発かいはつ共通きょうつう言語げんごがない

デザイナーは「商品しょうひんカード」や「ステータスチップ」という言葉ことば使つかう。コードがっているのはtemplates/catalog/list.html.twigだけだ。この共有きょうゆう語彙ごい欠如けつじょが、デザインレビューのたびに翻訳ほんやく作業さぎょう発生はっせいさせる。

Note

この5つの症状しょうじょうは、サーバーがわのモノリシックなコントローラーで批判ひはんされるものと、まったくおなじものがUIがわあらわれているにすぎない。混在こんざいした責務せきむ重複じゅうふく単体たんたいテストの不可能ふかのうせいヘキサゴナルアーキテクチャっているひとなら、おな既視感きしかんおぼえるはずだ。


2. Atomic Designとはなに

目指めざすのは、ページを設計せっけいすることをやめて、システムを設計せっけいはじめることだ。ページは設計せっけい単位たんいであることをやめ、よりちいさなコンポーネントをてた結果けっかになる。そのコンポーネントもまた、さらにちいさなコンポーネントからてられている。

Brad Frostは化学かがくのメタファーをりている。物質ぶっしつ原子げんし(atom)でできていて、原子げんし結合けつごうして分子ぶんし(molecule)になり、分子ぶんし有機ゆうきたい(organism)を形作かたちづくる。どのレベルも恣意しいてきなものではない。それぞれが複雑ふくざつさと具体ぐたいせいことなる度合どあいをあらわしている。

5つのレベル

1. アトム(原子げんし

ボタン、入力にゅうりょくらん、ラベル、アイコン、見出みだしといった、それ以上いじょう分割ぶんかつできない構成こうせい要素ようそ。アトム単体たんたいには機能きのうてき意味いみがない(ラベルのない入力にゅうりょくらんやくたない)が、それでもプロダクトの視覚しかくてきアイデンティティをまるごとになっている。

  • ビジネスロジックを一切いっさいふくまない。
  • APIも、ストアも、現在げんざいのルートもらない。
  • 完全かんぜんにpropsや属性ぞくせいによって駆動くどうされる。

2. 分子ぶんし(Molecule)

分子ぶんしは、ひとつのまとまったタスクげるためにアトムをわせたものだ。ラベル+入力にゅうりょくらん+エラーメッセージでFormFieldになる。入力にゅうりょくらん+ボタンでSearchFieldになる。

これはインターフェースがはじめて*使つかえる*ものになるレベルだ。分子ぶんしはローカルなUI状態じょうたい開閉かいへい、ホバー)をつことはあっても、ビジネスロジックはまだたない。

3. ゆう機体きたい(Organism)

ゆう機体きたいは、比較的ひかくてき複雑ふくざつ自己じこ完結かんけつしたインターフェースのいち区画くかくだ。サイトヘッダー、商品しょうひんカード、結果けっか一覧いちらんグリッド、完成かんせいしたフォームなど。分子ぶんしとアトムをわせて構成こうせいされる。

**ビジネス語彙ごい**が正当せいとう登場とうじょうはじめるのはここからだ。ゆう機体きたいProductCard名付なづけられ、Productオブジェクトをることができる。プロダクト固有こゆうではあるが、ページをまたいでさい利用りよう可能かのう状態じょうたいたもっている。

4. テンプレート(Template)

テンプレートはページの骨組ほねぐみであり、じつデータをたずにレイアウト有機ゆうきたい配置はいち定義ていぎする。ワイヤーフレームのコードばんっていい。

その役割やくわりは、コンテンツとは独立どくりつに、構造こうぞう密度みつど・レスポンシブ挙動きょどう検証けんしょうすることだ。

5. ページ(Page)

ページはテンプレートの具体ぐたいてきなインスタンスであり、じつデータによってたされる。そと世界せかい接続せつぞくする唯一ゆいいつのレベルだ。ルーティング、データ取得しゅとく、SEOメタデータ、グローバルな状態じょうたい

システムの堅牢けんろうせいためされるのもこのレベルだ。商品しょうひんめいが200文字もじだったら?リストがそらだったら?画像がぞうつからなかったら?


依存いぞんしも方向ほうこうにしかかないという法則ほうそく

ヘキサゴナルアーキテクチャは依存いぞんせい逆転ぎゃくてん原則げんそくうえっている。Atomic Designもおなじくらいみじかく、おなじくらい頻繁ひんぱんやぶられるルールのうえっている。

Important

コンポーネントは厳密げんみつ下位かいのレベルのコンポーネントしかわせてはならず、自分じぶんがどこで使つかわれるかを一切いっさいってはならない。

ここから実務じつむじょう帰結きけつが2つみちびかれ、それこそがこのモデルの価値かちのすべてだとっていい。

1. 依存いぞんしも方向ほうこうにしかかない。 アトムはどの分子ぶんしらない。分子ぶんしはどの有機ゆうきたいらない。ゆう機体きたいがページをインポートすることはない。このルールは静的せいてき検証けんしょう可能かのうであり、ヘキサゴンにおけるレイヤールールとまったくおなじだ(自動じどう方法ほうほう後述こうじゅつする)。

2. したくほど純度じゅんどがる。 階層かいそうしたにいるコンポーネントほど、より汎用はんようてきで、安定あんていしていて、さい利用りようしやすい。うえにいくほど、より特化とっかしていて、揮発きはつせいたかく、外部がいぶ接続せつぞくしている。

レベルビジネスロジックデータアクセスさい利用りようせい変更へんこう頻度ひんど
アトム❌ なし❌ なし万能ばんのうきわめてまれ
分子ぶんし❌ なし❌ なしたかまれ
ゆう機体きたい⚠️ 表示ひょうじレベルのみ⚠️ できればpropsでちゅう程度ていど定期ていきてき
テンプレート❌ なしかりデータのみひく定期ていきてき
ページ✅ オーケストレーション✅ ありなし頻繁ひんぱん

これはヘキサゴンとまったくおなうごきだ。安定あんていしているものを、揮発きはつせいのあるものからはなす。 アプリケーションコアがビジネスルールを技術ぎじゅつてき詳細しょうさいからまもるように、アトムはページのまぐれから視覚しかくてきアイデンティティをまもる。

Note

Brad Frostは、わすれられがちなてん強調きょうちょうしている。Atomic Designは**直線ちょくせんてきなプロセスではない**。まずすべてのアトムを設計せっけいし、つぎにすべての分子ぶんし設計せっけいする、というものではない。ページのモックアップから出発しゅっぱつしてコンポーネントを抽出ちゅうしゅつするなど、レベルかんえずする。このモデルはレンズであって、順序じゅんじょった方法ほうほうろんではない。

以降いこう記事きじでは、このスパゲッティじょうのテンプレートをシステムへとつくなおし、両方りょうほう技術ぎじゅつかくレベルを並行へいこうして構築こうちくしていく。


3. レベルゼロ:デザイントークン

アトムよりまえに、アトムがなんでできているかをめておく必要ひつようがある。#2563ebをべたきしたあおいボタンはアトムではなく、姿すがたえたマジックナンバーにすぎない。

デザイントークンとは、いろ余白よはく・タイポグラフィ・角丸かどまるかげ名前なまえけたのことだ。デザインとコードの契約けいやくえる。

もとのCSSで、TwigからもVueからも使つかえる

/* assets/styles/tokens.css */
:root {
    /* Semantic colors — never a raw color name inside components */
    --color-brand: #2563eb;
    --color-brand-hover: #1d4ed8;
    --color-surface: #ffffff;
    --color-text: #1a1a1a;
    --color-text-muted: #6b7280;
    --color-success-bg: #dcfce7;
    --color-success-text: #166534;
    --color-danger-bg: #fee2e2;
    --color-danger-text: #991b1b;

    /* Spacing scale — no arbitrary values */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-6: 1.5rem;

    /* Typography */
    --font-size-sm: 0.875rem;
    --font-size-base: 1rem;
    --font-size-lg: 1.125rem;
    --font-size-xl: 1.5rem;

    /* Shapes */
    --radius-md: 0.375rem;
    --radius-full: 9999px;
}

[data-theme="dark"] {
    --color-surface: #111827;
    --color-text: #f9fafb;
    --color-text-muted: #9ca3af;
}

Vueがわでは、UnoCSSで

// unocss.config.ts
import { defineConfig } from 'unocss'

export default defineConfig({
  theme: {
    colors: {
      brand: {
        DEFAULT: 'var(--color-brand)',
        hover: 'var(--color-brand-hover)',
      },
      surface: 'var(--color-surface)',
    },
  },
})

Tip

いトークンシステムの決定的けっていてきなテストがある。コンポーネントのフォルダない#検索けんさくして、なにもヒットしないこと。アトムのなかつかったリテラルないろは、まだ名前なまえけられていないトークンだ。くのは些細ささいだが、おどろくほど効果こうかてきなlintルールになる。

ひと大事だいじなニュアンスとして、トークンには**役割やくわり**で名前なまえけること(--color-danger-bg)。名前なまえけてはいけない(--color-red-100)。そうしないと、あかがオレンジになったに、redという名前なまえのトークンが#f97316保持ほじしているという事態じたいになる。


4. 実践じっせんへん:アトム

いよいよリファクタリングだ。14かいなおされたあのプライマリボタンを、ひとつのアトムにする。

アトムを設計せっけいするさいのルール

アトムは状態じょうたい記述きじゅつするプロパティしか公開こうかいしてはならず、自分じぶん使つかわれる文脈ぶんみゃく公開こうかいしてはならない。消費しょうひするのはデザイントークンだけであり、それ以外いがいなにもない。そして能動のうどうてきなにかをするのではなく、イベントを発行はっこうする。addToCartではなくclickだ。

アトムは外部がいぶマージンを一切いっさいたず、ストアにもルートにもAPIにもれず、ビジネス由来ゆらい名前なまえたない。CheckoutButtonわるいアトムめい典型てんけいだ。

Important

外部がいぶマージン禁止きんしのルールは、もっと頻繁ひんぱんやぶられ、もっともコストがかかるものだ。margin-bottom: 16px宣言せんげんしたアトムは、そのレイアウトをすべてのおやけることになる。水平すいへい方向ほうこうのツールバーに配置はいちしたには、margin-bottom: 0 !importantとのたたかいがはじまる。アトムにぞくするのは*ないがわのpaddingであり、要素ようそかん*の余白よはくはコンテナがわ責任せきにんであって、理想りそうてきにはgap表現ひょうげんする。

Symfonyがわ無名むめいのTwigコンポーネント

Symfony UX Twig Componentsでは、ロジックさえなければPHPクラスを一切いっさいかずにコンポーネントを宣言せんげんできる。それはまさにアトムの状況じょうきょうそのものだ。

{# templates/components/Atom/Button.html.twig #}
{% props variant = 'primary', size = 'md', type = 'button', disabled = false %}

{% set variants = {
    primary:   'bg-brand text-white hover:bg-brand-hover',
    secondary: 'bg-transparent text-brand border border-brand hover:bg-brand/5',
    ghost:     'bg-transparent text-muted hover:bg-black/5',
} %}

{% set sizes = {
    sm: 'text-sm px-3 py-1.5',
    md: 'text-base px-4 py-2',
    lg: 'text-lg px-6 py-3',
} %}

<button
    type="{{ type }}"
    {{ disabled ? 'disabled' : '' }}
    {{ attributes.defaults({
        class: 'inline-flex items-center justify-center gap-2 rounded-md font-medium
                transition-colors disabled:opacity-50 disabled:cursor-not-allowed
                focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand
                ' ~ variants[variant] ~ ' ' ~ sizes[size]
    }) }}
>
    {% block content %}{% endblock %}
</button>

使つかかた

<twig:Atom:Button variant="secondary" size="sm">Cancel</twig:Atom:Button>
<twig:Atom:Button type="submit">Confirm</twig:Atom:Button>

{{ attributes.defaults({...}) }}注目ちゅうもくしてほしい。これのおかげで、がわdata-*aria-*、Stimulusの属性ぞくせいなどを、アトムがわがその存在そんざいることなくわたせる。これがなければ、あたらしい要件ようけんるたびにアトムへpropを追加ついかすることになる。

Vue 3:おなじアトムをSFCとして

<!-- src/components/atoms/AButton.vue -->
<script setup lang="ts">
interface Props {
  variant?: 'primary' | 'secondary' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
}

const { variant = 'primary', size = 'md', disabled = false } = defineProps<Props>()

const variants = {
  primary: 'bg-brand text-white hover:bg-brand-hover',
  secondary: 'bg-transparent text-brand border border-brand hover:bg-brand/5',
  ghost: 'bg-transparent text-muted hover:bg-black/5',
} as const

const sizes = {
  sm: 'text-sm px-3 py-1.5',
  md: 'text-base px-4 py-2',
  lg: 'text-lg px-6 py-3',
} as const
</script>

<template>
  <button
    :disabled="disabled"
    class="inline-flex items-center justify-center gap-2 rounded-md font-medium
           transition-colors disabled:opacity-50 disabled:cursor-not-allowed
           focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
    :class="[variants[variant], sizes[size]]"
  >
    <slot />
  </button>
</template>

使つかかた

<AButton variant="secondary" size="sm">Cancel</AButton>
<AButton @click="submit">Confirm</AButton>

Note

2つの実装じっそう構造こうぞうてきにまったく同一どういつだ。propsもおなじ、バリアントもおなじ、クラスもおなじ、スロットもおなじ。ちがうのは構文こうぶんだけ。Atomic Designが記述きじゅつしているのはアーキテクチャであってテクノロジーではない。だからこそ、TwigからVueへ移行いこうするチームは、システム全体ぜんたいかんがなおすことなく、コンポーネント単位たんい移行いこうできる。

2つのアトム:バッジ

{# templates/components/Atom/Badge.html.twig #}
{% props tone = 'neutral' %}

{% set tones = {
    neutral: 'bg-gray-100 text-gray-700',
    success: 'bg-success-bg text-success-text',
    danger:  'bg-danger-bg text-danger-text',
} %}

<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium {{ tones[tone] }}">
    {% block content %}{% endblock %}
</span>
<!-- src/components/atoms/ABadge.vue -->
<script setup lang="ts">
const { tone = 'neutral' } = defineProps<{
  tone?: 'neutral' | 'success' | 'danger'
}>()

const tones = {
  neutral: 'bg-gray-100 text-gray-700',
  success: 'bg-success-bg text-success-text',
  danger: 'bg-danger-bg text-danger-text',
} as const
</script>

<template>
  <span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium" :class="tones[tone]">
    <slot />
  </span>
</template>

命名めいめい注目ちゅうもくしてほしい。tone="danger"であってcolor="red"ではない。アトムが公開こうかいしているのは意図いとであって、視覚しかくてきではない。デザインが「danger」をオレンジにするとめたがわなにえる必要ひつようがない。


5. 分子ぶんし:ひとつのタスクのためにてる

分子ぶんしはアトムをわせて、ひとつのことをげる。これは分子ぶんし有機ゆうきたい見分みわけるもっと信頼しんらいできるテストでもある。「〜と〜」とわずにその役割やくわり一文いちぶん説明せつめいできないなら、それはおそらくゆう機体きたいだ。

StockBadgeなまデータから視覚しかくてき意図いと

legacyなテンプレートには、在庫ざいこかんするif/elseがあちこちに重複じゅうふくしていた。これはまさに分子ぶんしだ。データを視覚しかく表現ひょうげん変換へんかんする。

{# templates/components/Molecule/StockBadge.html.twig #}
{% props stock %}

{% if stock > 10 %}
    <twig:Atom:Badge tone="success">In stock</twig:Atom:Badge>
{% elseif stock > 0 %}
    <twig:Atom:Badge tone="neutral">Only {{ stock }} left</twig:Atom:Badge>
{% else %}
    <twig:Atom:Badge tone="danger">Out of stock</twig:Atom:Badge>
{% endif %}
<!-- src/components/molecules/MStockBadge.vue -->
<script setup lang="ts">
const { stock } = defineProps<{ stock: number }>()
</script>

<template>
  <ABadge v-if="stock > 10" tone="success">In stock</ABadge>
  <ABadge v-else-if="stock > 0" tone="neutral">Only {{ stock }} left</ABadge>
  <ABadge v-else tone="danger">Out of stock</ABadge>
</template>

Tip

> 10というしきいは、分子ぶんしまぎんでしまったビジネスルールだ。厳密げんみつえば、この計算けいさんはドメインがわぞくするべきであり、分子ぶんしはすでに決定けっていみのステータス(status: 'in_stock' | 'low' | 'out')をるべきだ。これはよくある現実げんじつてき妥協だきょうてんだ。表示ひょうじルールが些細ささいなものであれば許容きょようできるが、しきい設定せってい可能かのうになったり顧客こきゃく依存いぞんになったりした瞬間しゅんかん拒否きょひすべきものになる。

PriceTag:フォーマットをいち箇所かしょ集約しゅうやくする

{# templates/components/Molecule/PriceTag.html.twig #}
{% props amountCents, currency = 'USD', size = 'md' %}

{% set sizes = { sm: 'text-sm', md: 'text-xl', lg: 'text-3xl' } %}

<p class="font-bold text-brand {{ sizes[size] }}">
    {{ (amountCents / 100)|format_currency(currency) }}
</p>
<!-- src/components/molecules/MPriceTag.vue -->
<script setup lang="ts">
const { amountCents, currency = 'USD', size = 'md' } = defineProps<{
  amountCents: number
  currency?: string
  size?: 'sm' | 'md' | 'lg'
}>()

const sizes = { sm: 'text-sm', md: 'text-xl', lg: 'text-3xl' } as const

const formatted = computed(() =>
  new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amountCents / 100),
)
</script>

<template>
  <p class="font-bold text-brand" :class="sizes[size]">{{ formatted }}</p>
</template>

通貨つうかのフォーマットは、スタックごとにちょうど1箇所かしょだけに存在そんざいするようになった。通貨つうか追加ついかする、ロケールをえる、「税抜ぜいぬき/税込ぜいこみ」を表示ひょうじする、いずれも1ファイルで完結かんけつする。

FormField教科書きょうかしょてき事例じれい

<!-- src/components/molecules/MFormField.vue -->
<script setup lang="ts">
const { label, error, hint, required = false } = defineProps<{
  label: string
  error?: string
  hint?: string
  required?: boolean
}>()

const id = useId()
const describedBy = computed(() =>
  [error && `${id}-error`, hint && `${id}-hint`].filter(Boolean).join(' ') || undefined,
)
</script>

<template>
  <div class="flex flex-col gap-1">
    <ALabel :for="id" :required="required">{{ label }}</ALabel>

    <slot :id="id" :described-by="describedBy" :invalid="!!error" />

    <p v-if="hint && !error" :id="`${id}-hint`" class="text-sm text-muted">
      {{ hint }}
    </p>
    <p v-if="error" :id="`${id}-error`" class="text-sm text-danger-text" role="alert">
      {{ error }}
    </p>
  </div>
</template>

この分子ぶんしは、このレベルでしかになえない責務せきむっている。関係かんけいせいとしてのアクセシビリティだ。ラベルとフィールドのむすびつき(for/id)、フィールドとエラーメッセージのむすびつき(aria-describedby)は、にしか存在そんざいない。どのアトムも単独たんどくでこれをあつかうことはできない。

これはこのモデル全体ぜんたいにとって過小かしょう評価ひょうかされている論点ろんてんだ。すべてのページが手作業てさぎょうでフィールドをたてなおすシステムにおいて、ただしいアクセシビリティを保証ほしょうすることは構造こうぞうてき不可能ふかのうだ。分子ぶんし集約しゅうやくすれば、一度いちどれればそれでむ。


6. ゆう機体きたい:ビジネスの登場とうじょう

ゆう機体きたいはインターフェースの自己じこ完結かんけつしたいち区画くかくだ。ビジネスデータのかたちることがゆるされる最初さいしょのレベルである。

ProductCard

{# templates/components/Organism/ProductCard.html.twig #}
{% props product %}

<article class="flex flex-col gap-3 rounded-lg border border-gray-200 bg-surface p-4 shadow-sm">
    <img
        src="{{ product.imageUrl }}"
        alt="{{ product.name }}"
        loading="lazy"
        class="h-48 w-full rounded object-cover"
    >

    <div class="flex items-start justify-between gap-2">
        <h3 class="text-lg font-semibold">{{ product.name }}</h3>
        <twig:Molecule:StockBadge :stock="product.stock" />
    </div>

    <p class="text-sm text-muted">{{ product.description|u.truncate(80, '') }}</p>

    <twig:Molecule:PriceTag :amountCents="product.priceCents" />

    <twig:Atom:Button
        class="mt-auto w-full"
        :disabled="product.stock == 0"
        data-action="cart#add"
        data-cart-product-id-param="{{ product.id }}"
    >
        Add to cart
    </twig:Atom:Button>
</article>
<!-- src/components/organisms/OProductCard.vue -->
<script setup lang="ts">
import type { Product } from '~/types/catalog'

const { product } = defineProps<{ product: Product }>()
const emit = defineEmits<{ addToCart: [productId: string] }>()
</script>

<template>
  <article class="flex flex-col gap-3 rounded-lg border border-gray-200 bg-surface p-4 shadow-sm">
    <img
      :src="product.imageUrl"
      :alt="product.name"
      loading="lazy"
      class="h-48 w-full rounded object-cover"
    >

    <div class="flex items-start justify-between gap-2">
      <h3 class="text-lg font-semibold">{{ product.name }}</h3>
      <MStockBadge :stock="product.stock" />
    </div>

    <p class="text-sm text-muted line-clamp-2">{{ product.description }}</p>

    <MPriceTag :amount-cents="product.priceCents" />

    <AButton
      class="mt-auto w-full"
      :disabled="product.stock === 0"
      @click="emit('addToCart', product.id)"
    >
      Add to cart
    </AButton>
  </article>
</template>

ここでまる価値かちのあるてんが2つある。

ゆう機体きたいはアクションを実行じっこうするのではなく、シグナルとしてらせるだけだ。VueがわではaddToCartをemitし、TwigがわではStimulusコントローラーに属性ぞくせい経由けいゆ処理しょり委譲いじょうする。どちらの場合ばあいも、ゆう機体きたい/api/cart/add存在そんざいすることすららない。そのおかげで、バックエンドが一切いっさいうごいていないドキュメントやテストやモックアップのなかでも描画びょうが可能かのう状態じょうたいたもっている。これはヘキサゴンにおける依存いぞんせい逆転ぎゃくてんが、インターフェースがわうつってきたものだ。コンポーネントは必要ひつようとするものを宣言せんげんし、がわがその実装じっそう供給きょうきゅうする。

このファイルない唯一ゆいいつmarginはボタンのmt-autoだが、これは正当せいとうなものだ。カード自身じしんおやとして、自分じぶんのボタンを下端かたんしやるとめているからだ。外部がいぶマージン禁止きんしのルールが規定きていしているのは、コンポーネントと*そのおや*との関係かんけいであって、自分じぶん自身じしん境界きょうかい内側うちがわきることではない。

ProductGrid

<!-- src/components/organisms/OProductGrid.vue -->
<script setup lang="ts">
import type { Product } from '~/types/catalog'

const { products, loading = false } = defineProps<{
  products: Product[]
  loading?: boolean
}>()
defineEmits<{ addToCart: [productId: string] }>()
</script>

<template>
  <div v-if="loading" class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
    <MCardSkeleton v-for="i in 6" :key="i" />
  </div>

  <MEmptyState
    v-else-if="products.length === 0"
    title="No products"
    description="Try widening your search criteria."
  />

  <div v-else class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
    <OProductCard
      v-for="product in products"
      :key="product.id"
      :product="product"
      @add-to-cart="$emit('addToCart', $event)"
    />
  </div>
</template>

この有機ゆうきたいは、ページがわかえすべきではないものをになっている。コレクションの3つの状態じょうたい、ローディング、そら、データありだ。legacyなコードでは、そら状態じょうたいとローディング状態じょうたいはそもそも存在そんざいしなかった。たん白紙はくしのページとしてあらわれるだけだった。ゆう機体きたいまれることで、わすれることが構造こうぞうてき不可能ふかのうになる。


7. テンプレートとページ:まず構造こうぞう、それからデータ

テンプレート:コンテンツなしのレイアウト

<!-- src/components/templates/TCatalogLayout.vue -->
<template>
  <div class="mx-auto grid max-w-7xl gap-8 px-6 py-8 lg:grid-cols-[16rem_1fr]">
    <aside class="hidden lg:block">
      <slot name="filters" />
    </aside>

    <main class="flex flex-col gap-6">
      <header class="flex flex-wrap items-center justify-between gap-4">
        <slot name="title" />
        <slot name="toolbar" />
      </header>

      <slot name="results" />

      <footer class="flex justify-center">
        <slot name="pagination" />
      </footer>
    </main>
  </div>
</template>

このファイルにはデータも、インポートも、ロジックも一切いっさいふくまれていない。区画くかくとそのレスポンシブな挙動きょどう記述きじゅつするだけだ。最初さいしょゆう機体きたいすら存在そんざいしない段階だんかいで、灰色はいいろのブロックでこれを検証けんしょうできる。

Twigばんは、言語げんごがすでに用意よういしているブロック機能きのう使つかう。

{# templates/components/Template/CatalogLayout.html.twig #}
<div class="mx-auto grid max-w-7xl gap-8 px-6 py-8 lg:grid-cols-[16rem_1fr]">
    <aside class="hidden lg:block">
        {% block filters %}{% endblock %}
    </aside>

    <main class="flex flex-col gap-6">
        <header class="flex flex-wrap items-center justify-between gap-4">
            {% block title %}{% endblock %}
            {% block toolbar %}{% endblock %}
        </header>

        {% block results %}{% endblock %}

        <footer class="flex justify-center">
            {% block pagination %}{% endblock %}
        </footer>
    </main>
</div>

ページ:そと世界せかいとの唯一ゆいいつ接点せってん

<!-- pages/catalog.vue -->
<script setup lang="ts">
const { products, loading, filters } = useCatalog()
const cart = useCartStore()

useHead({ title: 'Catalog — Our products' })
</script>

<template>
  <TCatalogLayout>
    <template #filters>
      <OFilterPanel v-model="filters" />
    </template>

    <template #title>
      <AHeading level="1">Our products</AHeading>
    </template>

    <template #toolbar>
      <MSortSelect v-model="filters.sort" />
    </template>

    <template #results>
      <OProductGrid
        :products="products"
        :loading="loading"
        @add-to-cart="cart.add"
      />
    </template>

    <template #pagination>
      <MPagination v-model="filters.page" :total="products.length" />
    </template>
  </TCatalogLayout>
</template>

ページは配線はいせんファイルになった。CSSクラスも、ifも、フォーマット処理しょりも、もうひとつもない。インフラのコントローラーがHTTPリクエストをユースケースに接続せつぞくするのとまったくおなじように、じつデータを既存きそん構造こうぞうにつなぎむだけだ。

だい1しょうのテンプレートとくらべてみてほしい。インラインスタイル、フォーマット、条件じょうけん分岐ぶんき、ネットワークしにまみれた45こうが、一目いちもくめる宣言せんげんわった。


8. ディレクトリ構成こうせい命名めいめい規則きそく

ディレクトリ構成こうせい

Symfonyがわ

templates/
├── components/
│   ├── Atom/
│   │   ├── Button.html.twig
│   │   ├── Badge.html.twig
│   │   ├── Input.html.twig
│   │   └── Label.html.twig
│   ├── Molecule/
│   │   ├── StockBadge.html.twig
│   │   ├── PriceTag.html.twig
│   │   └── FormField.html.twig
│   ├── Organism/
│   │   ├── ProductCard.html.twig
│   │   └── SiteHeader.html.twig
│   └── Template/
│       └── CatalogLayout.html.twig
└── pages/
    └── catalog/
        └── list.html.twig

src/Twig/Components/          <-- Only components that need logic
├── Molecule/
│   └── SearchField.php
└── Organism/
    └── CartSummary.php       <-- Live Component (server-side state)

Vueがわ

src/components/
├── atoms/
│   ├── AButton.vue
│   ├── ABadge.vue
│   └── AInput.vue
├── molecules/
│   ├── MStockBadge.vue
│   ├── MPriceTag.vue
│   └── MFormField.vue
├── organisms/
│   ├── OProductCard.vue
│   └── OProductGrid.vue
└── templates/
    └── TCatalogLayout.vue

pages/
└── catalog.vue               <-- The "Page" level, handled by the router

一文字ひともじ接頭せっとうA/M/O/T)は賛否さんぴかれる慣習かんしゅうだ。利点りてんは、コンポーネントのレベルが、それを使つか場所ばしょでファイルをひらかずとも一目いちもくでわかること。AButton.vueなか<OProductCard>かれていれば、レビューの違反いはん発見はっけんできる。

欠点けってんは、レベルがわったコンポーネントをリネームすると、すべてのもとれることになるてんだ。しかしそれこそがまさにのぞましいことでもある。レベルの変更へんこうは*アーキテクチャの変更へんこう*そのものであり、えるかたちになってしかるべきだ。

命名めいめい規則きそく

レベル名前なまえ由来ゆらいれいわるれい
アトムそのかたちButton, Input, IconCheckoutButton, UserAvatar
分子ぶんしそのタスクSearchField, PriceTagProductThing, Wrapper
ゆう機体きたいそのビジネス概念がいねんProductCard, SiteHeaderSection2, BigBox
テンプレートそのレイアウトCatalogLayout, ArticleLayoutPage1, MainTemplate

根底こんていにあるルールは、コンポーネントの名前なまえがその抽象ちゅうしょうのレベルを反映はんえいしていなければならないということだ。CheckoutButtonという名前なまえのアトムは、それが自分じぶん文脈ぶんみゃくっているという告白こくはくであり、つまりさい利用りよう不可能ふかのうであり、つまりそれはアトムではないということになる。


9. さらにふか

コンポーネントを単体たんたいでテストする

コンポーネントを文脈ぶんみゃくからはなすことで、だい1しょうでは不可能ふかのうだったことが可能かのうになる。アプリケーションを起動きどうせずにテストすることだ。

Vueがわ:VitestとTesting Library

// src/components/molecules/MStockBadge.test.ts
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
import MStockBadge from './MStockBadge.vue'

describe('mStockBadge', () => {
  it('signals availability above 10 units', () => {
    render(MStockBadge, { props: { stock: 42 } })
    expect(screen.getByText('In stock')).toBeTruthy()
  })

  it('warns on low stock', () => {
    render(MStockBadge, { props: { stock: 3 } })
    expect(screen.getByText('Only 3 left')).toBeTruthy()
  })

  it('signals depletion at zero', () => {
    render(MStockBadge, { props: { stock: 0 } })
    expect(screen.getByText('Out of stock')).toBeTruthy()
  })
})

SymfonyがわInteractsWithTwigComponents

Symfony UXは、コンポーネントをテストない単体たんたい描画びょうがするための専用せんようtraitを提供ていきょうしている。

<?php

declare(strict_types=1);

namespace App\Tests\Twig\Components;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\UX\TwigComponent\Test\InteractsWithTwigComponents;

final class ButtonTest extends KernelTestCase
{
    use InteractsWithTwigComponents;

    public function testRendersPrimaryVariantByDefault(): void
    {
        $rendered = $this->renderTwigComponent('Atom:Button', ['type' => 'submit']);

        self::assertStringContainsString('bg-brand', (string) $rendered);
        self::assertStringContainsString('type="submit"', (string) $rendered);
    }

    public function testDisabledStateIsExposedToAssistiveTechnology(): void
    {
        $rendered = $this->renderTwigComponent('Atom:Button', ['disabled' => true]);

        self::assertStringContainsString('disabled', (string) $rendered);
    }
}

Tip

こうしたテストはすうミリびょう完了かんりょうし、データベースもブラウザも認証にんしょうみセッションも必要ひつようとしない。50のコンポーネントからなるシステムでも、フルスイートが3びょう未満みまんわる。これこそが、おそれずにリファクタリングするために必要ひつようなフィードバックループだ。

ドキュメント:システムのショールーム

だれ参照さんしょうしないデザインシステムは、スプリントのたびにさい発明はつめいされる。スタックによって2つのアプローチがある。

Vueがわでは、Storybook(またはHistoire)がかくコンポーネントをあらゆるバリエーションで描画びょうがしてくれる。

// src/components/atoms/AButton.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import AButton from './AButton.vue'

const meta = {
  title: 'Atoms/Button',
  component: AButton,
  argTypes: {
    variant: { control: 'select', options: ['primary', 'secondary', 'ghost'] },
    size: { control: 'select', options: ['sm', 'md', 'lg'] },
  },
} satisfies Meta<typeof AButton>

export default meta

export const Primary: StoryObj<typeof meta> = {
  args: { variant: 'primary' },
  render: args => ({
    components: { AButton },
    setup: () => ({ args }),
    template: '<AButton v-bind="args">Add to cart</AButton>',
  }),
}

export const Disabled: StoryObj<typeof meta> = { args: { disabled: true } }

Symfonyがわでは、Storybookを統合とうごうすること自体じたい可能かのうだが、コストがおもい。開発かいはつ環境かんきょう限定げんていのショールームようルートを1ほん用意よういするほうがはるかに安上やすあがりだ。

<?php

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class DesignSystemController extends AbstractController
{
    #[Route('/_design-system', name: 'design_system', env: 'dev')]
    public function index(): Response
    {
        return $this->render('design_system/index.html.twig');
    }
}

対応たいおうするテンプレートは、すべてのアトムをあらゆるわせで描画びょうがする。Storybookよりは貧弱ひんじゃくだが、構築こうちくには1時間じかんもかからず、ビルドの依存いぞんえず、必要ひつようの9わりはこれでカバーできる。すべての状態じょうたい一目いちもく確認かくにんできることだ。

アーキテクチャを自動的じどうてき強制きょうせいする

しも方向ほうこう依存いぞん法則ほうそくは、コードレビューだけが唯一ゆいいつのチェック手段しゅだんだと、納期のうきのプレッシャーのまえではのこれない。ヘキサゴンのときとおなじで、CIでブロッキングにする必要ひつようがある。

TypeScriptがわeslint-plugin-boundaries

// eslint.config.js
import boundaries from 'eslint-plugin-boundaries'

export default [
  {
    plugins: { boundaries },
    settings: {
      'boundaries/elements': [
        { type: 'atoms', pattern: 'src/components/atoms/*' },
        { type: 'molecules', pattern: 'src/components/molecules/*' },
        { type: 'organisms', pattern: 'src/components/organisms/*' },
        { type: 'templates', pattern: 'src/components/templates/*' },
        { type: 'pages', pattern: 'pages/*' },
      ],
    },
    rules: {
      'boundaries/element-types': ['error', {
        default: 'disallow',
        rules: [
          // An atom composes nothing: it is terminal.
          { from: 'atoms', allow: [] },
          { from: 'molecules', allow: ['atoms'] },
          { from: 'organisms', allow: ['atoms', 'molecules'] },
          { from: 'templates', allow: [] },
          { from: 'pages', allow: ['atoms', 'molecules', 'organisms', 'templates'] },
        ],
      }],
    },
  },
]

アトムから有機ゆうきたいをインポートしようとすると、これ以降いこうはlintが、つまりCIが失敗しっぱいするようになる。

PHPがわ:Deptrac、ただし重要じゅうよう注意ちゅういてんあり

DeptracはPHPの名前なまえ空間くうかん対象たいしょう判定はんていするため、クラスをつコンポーネントは完璧かんぺきにカバーできる。

# deptrac.yaml
deptrac:
  paths:
    - src/Twig/Components/
  layers:
    - name: Atom
      collectors:
        - { type: directory, value: src/Twig/Components/Atom/.* }
    - name: Molecule
      collectors:
        - { type: directory, value: src/Twig/Components/Molecule/.* }
    - name: Organism
      collectors:
        - { type: directory, value: src/Twig/Components/Organism/.* }
  ruleset:
    Atom: ~              # An atom depends on no other component
    Molecule:
      - Atom
    Organism:
      - Atom
      - Molecule

Warning

っておくべき制約せいやく *無名むめい*のTwigコンポーネントにはPHPクラスが存在そんざいしない。その依存いぞん関係かんけい.twigファイルない<twig:Organism:ProductCard />というタグのなか存在そんざいするだけで、Deptracからは完全かんぜんえない。そして、もっとまもるべきアトムや分子ぶんしこそが、もっと無名むめいコンポーネントになりがちなのだ。

これをおぎなう、些細ささいだが効果こうかてきなチェックがこのギャップをめる。

#!/usr/bin/env bash
# bin/check-atomic-boundaries.sh
set -euo pipefail

status=0

# An atom must not reference any higher-level component.
if grep -rlE '<twig:(Molecule|Organism|Template):' templates/components/Atom/ 2>/dev/null; then
    echo "❌ An atom composes a higher-level component." >&2
    status=1
fi

# A molecule must reference neither organisms nor templates.
if grep -rlE '<twig:(Organism|Template):' templates/components/Molecule/ 2>/dev/null; then
    echo "❌ A molecule composes a higher-level component." >&2
    status=1
fi

# No literal color may remain inside components.
if grep -rnE '#[0-9a-fA-F]{3,8}\b' templates/components/ 2>/dev/null; then
    echo "❌ Literal color detected: use a design token." >&2
    status=1
fi

exit $status

CIにまれた20こうのシェルスクリプトは、だれもがっていてだれまもらない規約きやくよりやくつ。

もっともコストのたかいアンチパターン

1. 分類ぶんるいがくてき麻痺まひ

症状しょうじょうUserAvatar分子ぶんしなのかゆう機体きたいなのか、チームが30ふん議論ぎろんする。

これがもっともありがちで、もっと不毛ふもうわなだ。Brad Frost自身じしんかえべている。分類ぶんるいはコミュニケーションのための道具どうぐであって、科学かがくではない。エスカレーション解除かいじょルールを採用さいようしよう。議論ぎろんが2ふんえたら、コンポーネントを上位じょういのレベルにいてさきすすむ。分類ぶんるい間違まちがえたコンポーネントの代償だいしょうはファイルの移動いどうひとつだが、毎週まいしゅう分類ぶんるい会議かいぎはプロジェクトそのものの代償だいしょうになる。

2. 全知全能ぜんちぜんのうのアトム

<!-- ❌ Twenty-three boolean props: this button has absorbed every edge case -->
<AButton
  :is-loading="true" :is-icon-only="false" :is-full-width="true"
  :has-badge="true" :badge-count="3" :is-dropdown-trigger="false"
  :show-spinner-left="true" ...
/>

エッジケースがえるたびにpropが追加ついかされ、ついにはアトムがめもテストもできないものになり、理論りろんじょう2²³どおりのわせがまれる。処方箋しょほうせん設定せっていより構成こうせい、つまり少数しょうすう意味いみのあるバリアントと、それ以外いがいはすべてスロットにまかせることだ。

<!-- ✅ Variation goes through content, not props -->
<AButton variant="primary" size="lg" class="w-full">
  <ASpinner v-if="pending" />
  <IconCart v-else />
  Add to cart
</AButton>

3. 幽霊ゆうれい分子ぶんし

MButtonWrapper.vueというファイルがあり、その中身なかみ<AButton><slot /></AButton>だけ。これはなにしておらず、ナビゲーションに間接かんせつ参照さんしょう階層かいそうひとやし、コンポーネントツリーを混乱こんらんさせるだけだ。構造こうぞうも、いも、意味いみ追加ついかしないコンポーネントは、存在そんざいすべきではない。

4. レベルをまたぐprop drilling

currentUserをページから4つのレベルをてアトムまでわたすということは、分解ぶんかい仕方しかた間違まちがっているか、コンテキストの仕組しくみ(Vueのprovide/inject、Twigのグローバルコンテキスト変数へんすう)がけているかのどちらかだ。現在げんざいのユーザーを必要ひつようのあるアトムは、定義ていぎじょう、もはやアトムではない。

5. はやすぎるビジネス命名めいめい

atoms/なかかれた<CheckoutSubmitButton>。この名前なまえ自体じたい違反いはん物語ものがたっている。このアトムはチェックアウトのながれをってしまっている。ただしいかたちは、汎用はんようてき<AButton>であり、それをビジネス語彙ごい正当せいとうにな<OCheckoutForm>という有機ゆうきたい使つかう、という構図こうずだ。

のアプローチとの関係かんけい

Atomic Designはいくつかのきんえんのモデルと共存きょうぞんしており、どれがどのいにこたえているのかをっておくとやくつ。

Feature-Sliced Designは、抽象ちゅうしょうのレベルではなく*機能きのう*単位たんいでコードを整理せいりする。この2つは競合きょうごうしない。だい規模きぼなアプリケーションでは、横断おうだんてきなアトミックデザインシステム(FSDのshared/uiはまさにアトムと分子ぶんしそのものだ)のうえにフィーチャー単位たんい分割ぶんかつかさねる構成こうせいをよくかける。おそらくだい規模きぼにおいてもっと堅牢けんろうわせだろう。

ITCSSはCSSがわおな直感ちょっかんこたえている。汎用はんようから特化とっかへと、特異とくいせいじゅん整理せいりするというかんがかただ。UnoCSSやTailwindのようなアトミックなエンジンがあれば、このいはほぼ意味いみうしなう。トークンとコンポーネントのバリアントが、カスケードにってわるからだ。

そして3レベル構成こうせいのシステムもある。おおくの成熟せいじゅくしたチームは、モデルをprimitives / components / features平坦へいたんし、片側かたがわでアトムと分子ぶんしを、もう片側かたがわゆう機体きたいとテンプレートを統合とうごうしている。これは十分じゅうぶんにかなっている。価値かちしも方向ほうこう依存いぞん法則ほうそくそのものにあるのであって、階層かいそう正確せいかく段数だんすうにあるわけではない。30のコンポーネントしかないプロジェクトで5段階だんかいのレベルをもうけるのは、たんなる儀式ぎしきにすぎない。


いつ採用さいようすべきか、いつけるべきか

万能ばんのうやくになるアーキテクチャは存在そんざいしない。Atomic Designは実質じっしつてきなものをわりに、実質じっしつてきなコストをはらう。

られるもの:ボタンの定義ていぎがひとつだけになり、もひとつしかありえなくなる。最初さいしょおそいが徐々じょじょがっていく速度そくど最初さいしょのページをつくるのは時間じかんがかかるが、語彙ごいがすでにそろっているぶん以降いこうのページはどんどんはやくなる。デザイナーと開発かいはつしゃおなじものをおな名前なまえぶようになり、誤解ごかいそうがまるごとなくなる。アプリを起動きどうせずに、あらゆる状態じょうたい単体たんたい描画びょうができるコンポーネント。そして、アクセシビリティが分子ぶんしなかいちだけ解決かいけつされる。ARIAの関係かんけいせい、フォーカス管理かんり状態じょうたい管理かんりを、ページごとにさい発明はつめいする必要ひつようがなくなる。

はらうコスト:最初さいしょのページが表示ひょうじされるまえに、たとえひかえめなシステムであってもすうじゅうファイルが必要ひつようになる。間接かんせつ参照さんしょうえる。ページがどう描画びょうがされるかを理解りかいするには、いまや4つ5つのファイルをひら必要ひつようがあり、はモノリシックなテンプレートがあたえてくれていた全体ぜんたいぞううしなう。使つかわれることのないバリエーションを先回さきまわりしてつくりたくなる誘惑ゆうわくつねにつきまとう。そして継続けいぞくてき規律きりつ必要ひつようになる。自動じどうされた強制きょうせいがなければ、しも方向ほうこう依存いぞん法則ほうそくすうげつくずれる。

視覚しかくてき語彙ごい共有きょうゆうする画面がめんがたくさんあるアプリケーションで採用さいようしよう。ほとんどのSaaS、管理かんり画面がめん、ECサイトがこれに該当がいとうする。複数ふくすうのフロントエンド開発かいはつしゃ、あるいは複数ふくすうのチームが1つのプロダクトにかかわるときに採用さいようしよう。デザインが継続けいぞくてきなリデザインをかさねながら、なんねん使つかわれつづけるプロダクトで採用さいようしよう。そして、視覚しかくてき一貫いっかんせい契約けいやくじょう、あるいは規制きせいじょう要件ようけんになっている場所ばしょ採用さいようしよう。厳格げんかくなブランドガイドライン、あるいは操作そうさせいがリスク分析ぶんせき一部いちぶとなる医療いりょうけいソフトウェアなどだ。

すうページしかないパンフレットてきなサイトでは見送みおくろう。システムのコストがそれがささえるページ自体じたいのコストを上回うわまわってしまう。使つかてのプロトタイプでも見送みおくろう。速度そくど優先ゆうせんされ、システムはから抽出ちゅうしゅつすればいい。サードパーティのデザインシステムがすでに導入どうにゅうされているなら見送みおくろう。VuetifyやBootstrap、あるいは自社じしゃライブラリを使つかっているなら、アトムはすでに存在そんざいしているので、分子ぶんしレベルからはじめればいい。サードパーティのコンポーネントのうえ<AButton>さい構築こうちくするのは、たんなるさい抽象ちゅうしょうにすぎない。そして、リアルタイムダッシュボードの1画面がめんのような、きわめて特化とっかした単一たんいつのインターフェースでも見送みおくろう。共有きょうゆうできるものがなにもないからだ。


まとめ

インターフェースを特異とくいせいしていくレベルへと階層かいそうし、依存いぞんしも方向ほうこうだけにけさせることで、4つのものをれた。

プライマリボタンはいまや正確せいかくに1つだけ存在そんざいするので、角丸かどまるえるということは、23ではなく1のファイルを編集へんしゅうすることを意味いみする。すべてのレベルが、データベースもブラウザもなしに、ミリびょう単位たんい単体たんたい描画びょうができる。おなじシステムを、まったくおな構造こうぞうでTwigにもVueにも実装じっそうできたということは、このモデルがフレームワークではなくアーキテクチャを記述きじゅつしているということのあかしだ。そしてデザイナーと開発かいはつしゃは、ついにおな名前なまえおなじものをせるようになった。

あゆんできたみちかえってみよう。

以前いぜん(モノリシックなページ)以後いご(Atomic Design)
ブランドカラーをえる23ファイルにわたる検索けんさく置換ちかんトークン1つ
無効むこうされたボタンをアプリを起動きどうし、在庫ざいこれの商品しょうひんさがストーリー1つ、1びょう
価格かかくのフォーマット7かい重複じゅうふく分子ぶんし1つ
リストのそら状態じょうたい存在そんざいしない(白紙はくしのページ)ゆう機体きたい
フォームのアクセシビリティフィールドごとに毎回まいかいつくなおFormField獲得かくとく
たページを追加ついかする200こうをコピペゆう機体きたいを6てる
アーキテクチャを検証けんしょうする目視もくしでのコードレビューCIでブロッキングされるlint

Atomic Designは、最初さいしょによりおおくのファイルとよりおおくの規律きりつ要求ようきゅうする。その見返みかえりとして、コードベースのなか伝統でんとうてきもっとはや劣化れっかする資産しさんであるインターフェースが、価値かち摩耗まもうするのではなくがっていくシステムにわる。

この論理ろんりがどこかで見覚みおぼえがあるとしたら、それは偶然ぐうぜんではない。ヘキサゴナルアーキテクチャとまったくおなかんがかただ。安定あんていしているものを見極みきわめ、揮発きはつせいのあるものからはなし、依存いぞん安定あんていした方向ほうこうかわせる。アトムがデザインにとって意味いみは、ドメインがビジネスにとって意味いみおなじである。

> share on linkedin
>