zudo-doc
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

コンポーネントファースト戦略

作成 2026年3月16日更新 2026年6月20日Takeshi Takatsudo

zudo-docがカスタムCSSクラス名ではなく、ユーティリティクラスを持つコンポーネントを使用する理由。

zudo-docはコンポーネントファースト戦略に従います:UIは常にTailwindユーティリティクラスを持つコンポーネントとして表現します。カスタムCSSクラス名を別のスタイルシートで作成することはしません。

問題

ユーティリティCSSフレームワークとコンポーネントフレームワークを併用するプロジェクトでは、開発者は従来のCSSパターンに戻りがちです。コンポーネント内でユーティリティクラスを組み合わせる代わりに、.profile-card.btn-primary.sidebar-navといったカスタムCSSクラス名を別のスタイルシートやCSSモジュールで作成します。

これにより、コードベースが断片化します:

  • ユーティリティをインラインで使うコンポーネント

  • カスタムCSSクラスを導入するコンポーネント

  • 両方のアプローチを混在させるコンポーネント

ルール

コンポーネント自体が抽象化です。 .card.btn-primaryのようなCSSクラス名は不要です。コンポーネントがカプセル化を、ユーティリティクラスがスタイリングを担当します。

  • カードが必要? → ユーティリティクラスを持つ<Card>コンポーネントを作成

  • ボタンバリアント? → <Button variant="primary">コンポーネント

  • レイアウトパターン? → <PageLayout>コンポーネント

zudo-docでの実践

zudo-docはzfb上で動作するPreactコンポーネント.tsx)を、Tailwind CSS v4ユーティリティとともに使用します。サーバーレンダリングのみのコンポーネントでも、クライアントでハイドレートされるアイランドでも、同じルールが適用されます。

サーバーレンダリングコンポーネント

// packages/zudo-doc/src/footer/footer.tsx
export function Footer({ copyright }: Props) {
  return (
    <footer class="border-t border-muted bg-surface px-hsp-xl py-vsp-xl">
      <div
        class="text-center text-caption text-muted"
        dangerouslySetInnerHTML={{ __html: copyright }}
      />
    </footer>
  );
}

.footerクラスなし。footer.module.cssなし。コンポーネント抽象化です。

クライアントでハイドレートされるアイランド

インタラクティブなコンポーネントは、独自のuseEffectやイベントバインディングのコードを通じてzfbのアイランドランタイムに自身を登録しますが、同じユーティリティクラスのアプローチを使用します:

// src/components/sidebar-toggle.tsx
export function SidebarToggle({ label }: Props) {
  const [open, setOpen] = useState(false);
  return (
    <button
      class="lg:hidden flex items-center gap-hsp-xs text-fg"
      onClick={() => setOpen(!open)}
    >
      {label}
    </button>
  );
}

アンチパターン

zudo-docプロジェクトでCSSクラス名を作成してはいけません

/* 間違い — カスタムCSSクラスを作成しない */
.profile-card {
  display: flex;
  gap: 1rem;
  padding: 1.5rem;
}
.profile-card__name {
  font-size: 1.25rem;
  font-weight: 600;
}
// 間違い — カスタムクラス名はデザインシステムをバイパスする
<div class="profile-card">
  <h3 class="profile-card__name">{name}</h3>
</div>

代わりに:

// 正しい — ユーティリティクラス、コンポーネントが抽象化
<div class="flex gap-hsp-md p-hsp-lg">
  <h3 class="text-body font-semibold">{name}</h3>
</div>

バリアントはPropsで

CSSモディファイアクラス(.btn--primary.btn--secondary)ではなく、コンポーネントプロップスを使用:

function Button({ variant = "primary", children }) {
  const styles = {
    primary: "bg-accent text-bg hover:bg-accent-hover",
    secondary: "bg-surface text-fg border border-muted",
  };
  return (
    <button className={`${styles[variant]} font-semibold py-vsp-xs px-hsp-md rounded`}>
      {children}
    </button>
  );
}

使い方:

<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>

維持すべき.btn-primaryクラスはありません。variantプロップは型安全で、補完が効き、自己文書化されています。

コンポーネントの組み合わせ

複雑なレイアウトは、より多くのCSSを追加するのではなく、より小さなコンポーネントを組み合わせて構築します:

<div class="divide-y divide-muted">
  {users.map((user) => (
    <div class="flex items-center gap-hsp-md py-vsp-sm">
      <Avatar src={user.avatar} size="sm" />
      <div class="flex-1 min-w-0">
        <p class="text-small font-medium text-fg truncate">{user.name}</p>
        <p class="text-caption text-muted truncate">{user.email}</p>
      </div>
    </div>
  ))}
</div>

各要素(<Avatar>、リストレイアウト)はコンポーネントです。.user-list__item.user-list__avatarといったクラス名は不要です。

デザイントークンを使用

任意の値ではなく、プロジェクトトークンを常に使用:

// 間違い — 任意の値はデザインシステムをバイパスする
<div class="p-[1.2rem] text-[0.875rem]">

// 正しい — デザイントークンを使用
<div class="p-hsp-md text-small text-muted">

利用可能なトークンについてはデザインシステムを参照してください。

カスタムCSSが許容される場合

zudo-docでカスタムCSSが許容される唯一の場所はsrc/styles/global.cssです:

  • コンテンツタイポグラフィ.zd-contentクラスはMDXパイプラインが生成する要素をスタイリング

  • デザイントークン定義 — Tailwindトークンを登録する@themeブロック

それ以外のすべて(すべてのコンポーネント、レイアウト、UI要素)はユーティリティクラスを直接使用します。

ルールのまとめ

  1. 常にコンポーネントを作成 — CSSクラスではなく

  2. ユーティリティクラスを直接使用 — コンポーネントマークアップ内で

  3. CSSモジュールファイルやカスタムクラス名を作成しない

  4. バリアントにはプロップスを使用 — CSSモディファイアではなく

  5. コンポーネントを組み合わせる — より多くのCSSではなく、小さなコンポーネントから複雑なUIを構築

  6. プロジェクトトークンを使用text-fgbg-surfacep-hsp-md、任意の値ではなく

Revision History

Takeshi Takatsudo作成: 2026-03-17T01:50:11+09:00更新: 2026-06-20T07:19:27Z

AI Assistant

Ask a question about the documentation.