概要
CSS Modules(.module.css)を使い、クラス名の衝突を避けたスコープ付きスタイルで Button コンポーネントを実装する。composes による基底スタイルの再利用と、バリアントの出し分けパターンを示す。
インストール
# 追加インストールは不要
実装
ベースとなる共通スタイル
/* styles/base.module.css */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
border: none;
cursor: pointer;
transition: opacity 0.15s;
}
.button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
Button コンポーネントのスタイル
/* components/Button.module.css */
.base {
composes: button from "../styles/base.module.css";
}
.primary {
composes: base;
background-color: #2563eb;
color: #ffffff;
}
.primary:hover:not(:disabled) {
background-color: #1d4ed8;
}
.secondary {
composes: base;
background-color: #f3f4f6;
color: #1f2937;
border: 1px solid #d1d5db;
}
.secondary:hover:not(:disabled) {
background-color: #e5e7eb;
}
.danger {
composes: base;
background-color: #dc2626;
color: #ffffff;
}
.danger:hover:not(:disabled) {
background-color: #b91c1c;
}
.sm {
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
}
.lg {
padding: 0.75rem 1.5rem;
font-size: 1rem;
}
Button コンポーネント
// components/Button.tsx
import styles from "./Button.module.css";
type Variant = "primary" | "secondary" | "danger";
type Size = "sm" | "md" | "lg";
type Props = {
variant?: Variant;
size?: Size;
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
};
export function Button({
variant = "primary",
size = "md",
disabled,
children,
onClick,
}: Props) {
const classes = [
styles[variant],
size !== "md" ? styles[size] : "",
]
.filter(Boolean)
.join(" ");
return (
<button className={classes} disabled={disabled} onClick={onClick}>
{children}
</button>
);
}
Card コンポーネントの例
/* components/Card.module.css */
.card {
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1.25rem;
background-color: #ffffff;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
}
.title {
font-size: 1rem;
font-weight: 600;
color: #111827;
margin-bottom: 0.5rem;
}
.body {
font-size: 0.875rem;
color: #6b7280;
line-height: 1.6;
}
// components/Card.tsx
import styles from "./Card.module.css";
type Props = {
title: string;
children: React.ReactNode;
};
export function Card({ title, children }: Props) {
return (
<div className={styles.card}>
<h3 className={styles.title}>{title}</h3>
<div className={styles.body}>{children}</div>
</div>
);
}
使用例
// app/page.tsx
import { Button } from "@/components/Button";
import { Card } from "@/components/Card";
export default function Page() {
return (
<main style={{ padding: "2rem", maxWidth: "600px", margin: "0 auto" }}>
<Card title="ボタンサンプル">
<div style={{ display: "flex", gap: "0.75rem", flexWrap: "wrap" }}>
<Button variant="primary">送信する</Button>
<Button variant="secondary">キャンセル</Button>
<Button variant="danger" size="sm">削除</Button>
<Button variant="primary" disabled>無効</Button>
</div>
</Card>
</main>
);
}
ポイント
.module.cssファイルに書いたクラス名は、ビルド時にButton_primary__xxxxのようなユニークなクラス名に変換される。クラス名の衝突が起きないcomposes: base;で別クラスのスタイルを継承できる。from "..."を使うと別ファイルのクラスも参照できる- Next.js は
.module.cssを標準サポートしており、追加の設定やインストールは不要 - Tailwind との使い分け: Tailwind はユーティリティ CSS(クラスを並べる)、CSS Modules はセマンティックなクラス名(コンポーネントの意味を表す)。既存のデザインシステムや CSS を活かしたい場合に CSS Modules が有効
- TypeScript から
import styles from "./Button.module.css"でインポートすると、クラス名の補完が効く(styles.primary等)