コンポーネントファースト戦略
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/です:
コンテンツタイポグラフィ —
.zd-contentクラスはMDXパイプラインが生成する要素をスタイリングデザイントークン定義 — Tailwindトークンを登録する
@themeブロック
それ以外のすべて(すべてのコンポーネント、レイアウト、UI要素)はユーティリティクラスを直接使用します。
ルールのまとめ
常にコンポーネントを作成 — CSSクラスではなく
ユーティリティクラスを直接使用 — コンポーネントマークアップ内で
CSSモジュールファイルやカスタムクラス名を作成しない
バリアントにはプロップスを使用 — CSSモディファイアではなく
コンポーネントを組み合わせる — より多くのCSSではなく、小さなコンポーネントから複雑なUIを構築
プロジェクトトークンを使用 —
text-fg、bg-surface、p-hsp-md、任意の値ではなく