zudo-doc
GitHub リポジトリ

検索したい単語を入力

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

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

作成 2026年3月16日更新 2026年7月15日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ユーティリティとともに使用します。プロジェクトが所有する .tsx ファイルでは、class ではなく className を書いてください。scaffold は reactpreact/compat にマッピングするため、作成した JSX は React 互換の型を使い、classzfb check に失敗します。サーバーレンダリングのみのコンポーネントでも、クライアントでハイドレートされるアイランドでも、同じコンポーネントファーストのルールが適用されます。

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

ほとんどのUIはサーバーレンダリングです。JavaScriptを出力せず、ユーティリティクラスをインラインで使います。パッケージの packages/zudo-doc/src/footer/footer.tsx はサーバーレンダリングの例です。プロジェクトが所有するコンポーネントは次の形で書いてください。

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

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

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

インタラクティブなモジュールは "use client" で始め、ホストが @takazudo/zfbIsland() でコンポーネントをラップします。Island({ when, children }) はハイドレーションを始めるタイミング("load""idle""visible")を選び、必要なら ssrFallback でサーバーレンダリング時のマークアップを提供します。パッケージの packages/zudo-doc/src/header-with-defaults/index.tsx は、既存の SidebarToggle アイランドにこの呼び出し形式を使っています。必須の静的インポート経路と実験的なスキャナーの制約については、完全なインタラクティブアイランドのレシピに従ってください。

import { Island } from "@takazudo/zfb";

// Counter is imported from a module that starts with "use client".
const CounterIsland = () =>
  Island({
    when: "visible",
    ssrFallback: <span className="text-caption">Loading counter...</span>,
    children: <Counter />,
  });

アンチパターン

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

/* WRONG — don't create custom CSS classes */
.profile-card {
  display: flex;
  gap: 1rem;
  padding: 1.5rem;
}
.profile-card__name {
  font-size: 1.25rem;
  font-weight: 600;
}
// WRONG — custom class names bypass the design system
<div className="profile-card">
  <h3 className="profile-card__name">{name}</h3>
</div>

代わりに:

// RIGHT — utility classes, the component is the abstraction
<div className="flex gap-hsp-md p-hsp-lg">
  <h3 className="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 className="divide-y divide-muted">
  {users.map((user) => (
    <div className="flex items-center gap-hsp-md py-vsp-sm">
      <Avatar src={user.avatar} size="sm" />
      <div className="flex-1 min-w-0">
        <p className="text-small font-medium text-fg truncate">{user.name}</p>
        <p className="text-caption text-muted truncate">{user.email}</p>
      </div>
    </div>
  ))}
</div>

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

デザイントークンを使用

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

// WRONG — arbitrary values bypass the design system
<div className="p-[1.2rem] text-[0.875rem] text-[#6b7280]">

// RIGHT — use design tokens
<div className="p-hsp-md text-small text-muted">

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

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

プロジェクトレベルのCSSは src/styles/global.css に置きますが、パッケージが所有するスタイルをそこに複製してはいけません:

  • コンテンツタイポグラフィ.zd-contentpackages/zudo-doc/src/content.css で一度だけ定義され、@takazudo/zudo-doc/content.css として配布されます。scaffold は MDX タイポグラフィのルールをコピーせず、このパッケージスタイルシートをインポートします

  • プロジェクトのカスタマイズ@theme のオーバーライド、プロジェクト固有の機能スタイル、明示的なコンポーネントスロットのスタイルは src/styles/global.css に置きます

それ以外のすべて(プロジェクトが所有するコンポーネント、レイアウト、通常の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-07-16T06:32:22+09:00

AI Assistant

Ask a question about the documentation.

Preview theme

Loading theme previews…