Prompt
reactで、ロジックはほとんどなくほとんどCSSだけのボタンなどのユーティリティコンポーネントは作る?
あはは、それはフロントエンドエンジニアが100回くらい自問自答する「永遠のテーマ」だね!
結論から言うと、「作るべきだし、それが2026年現在のReact設計における『一意な正解』に近い」 と言えるよ。
ロジックがなくても、CSSの塊をコンポーネント化することには、単なる「共通化」以上の、工学的な堅牢性を高めるメリットがあるんだ。
1. なぜ「CSSだけのコンポーネント」を作るのか?
「CSSなんてクラス名を当てるだけでいいじゃないか」と思うかもしれないけれど、コンポーネント化することで 「スイスチーズの穴(意図しないスタイルの崩れ)」 を防げるようになる。
① 「役割」と「実装」を分離できる
例えば、ただの div に className="bg-red-500 rounded-full ..." と書くのではなく、<Badge color="red" /> という名前を付ける。
- メリット: 使う側(親コンポーネント)は「どんな見た目か」を意識せず、「何であるか(役割)」だけを記述すれば済む。コードの可読性が爆発的に上がるんだ。
② スタイルの「一貫性」を強制できる
もし生のCSSクラスを直接使っていると、誰かが「あ、このボタンだけちょっと角を丸くしたいな」と rounded-lg を勝手に足してしまうかもしれない(属人性)。
- メリット: コンポーネント化して
variantなどの Props 経由でしかスタイルを変えられないように制限をかけることで、デザインシステムという 「一意な境界線」 を守れるようになる。
2. 2026年流:ユーティリティコンポーネントの作り方
今は Tailwind CSS などのユーティリティファーストなCSSが主流だけど、それを React コンポーネントに昇華させる手法が定石だね。
cva (class-variance-authority) の活用
「ロジックはほとんどない」と言いつつ、「条件によってスタイルを切り替える」 という小さなロジックを美しく記述するために cva のようなライブラリを使うのが、今のトレンドだよ。
TypeScript
// Button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors", // 基本スタイル
{
variants: {
variant: {
primary: "bg-blue-600 text-white hover:bg-blue-700",
outline: "border border-gray-300 bg-transparent hover:bg-gray-100",
},
size: {
sm: "h-8 px-3",
md: "h-10 px-4",
},
},
defaultVariants: {
variant: "primary",
size: "md",
},
}
);
export const Button = ({ variant, size, className, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>) => {
return <button className={buttonVariants({ variant, size, className })} {...props} />;
};
3. 見落とされがちな視点: 「ポリモーフィズム」
CSS主体のコンポーネントを作る時にハマるのが、「ボタンの見た目だけど、実体は <a> タグ(リンク)にしたい」 というケース。
これを解決するのが 「asChild」パターン や Radix UI のようなアプローチだ。
- 穴の回避: 見た目(CSS)とセマンティクス(HTMLタグの役割)を分離して、
<Button asChild><a href="...">...</a></Button>と書けるようにしておく。これができるようになると、君のコンポーネントは真に「ユーティリティ」として完成するよ。
4. 開発効率と「切なさ」のバランス
君が前に言っていた「フルスクラッチの楽しさ」が恋しくなったら、こうしたユーティリティコンポーネントを 「外部ライブラリ(shadcn/uiなど)をコピペするのではなく、あえて一から自分で設計してみる」 のはどうだろう?
-
バニラJS時代の熱量: ライブラリを使わずに、いかに
clsxやtailwind-mergeを使って「完璧なクラス名結合ロジック」を自作するか。 -
今の楽しさ: その自作コンポーネントを AI に学習させて、爆速でUIを組み立てさせる。
結論
React でロジックのない CSS コンポーネントを作るのは、「未来の自分やチームに対して、壊れにくいインターフェースを提供する」 という極めて建設的な行為だ。
君の「部品を組み立てる楽しさ」を最大化するために、まずは 「自分だけの一意なボタンコンポーネント」 を一から削り出してみるのはどうかな?
次は、「作成したボタンに、React Aria などのライブラリを組み合わせて、『見た目はシンプルだけどアクセシビリティ(キーボード操作など)が完璧』な究極のコンポーネントに進化させる手順」 をやってみる?
Would you like me to code a complete utility component set (Button, Input, Badge) using the CVA pattern for your next project?
Prompt
ボタンにはイベントなども含まれるけど、propsにちょっと悩むことはない?
あはは、その悩みこそが 「コンポーネント設計の沼」 への入り口だね!
「ただのボタン」だと思って作り始めると、型定義(TypeScript)や Props の渡し方で、「どこまで自由にさせるべきか」というジレンマに必ず突き当たる。これは情報の 「カプセル化(隠蔽)」 と 「柔軟性」 のトレードオフの問題なんだ。
君が悩んでいるであろうポイントを、工学的な「一意な解決策」とともに整理してみよう。
1. 悩みの種: onClick や type をどう定義するか
一番やってはいけないのは、必要な Props を一つずつ手動で定義することだ。
onClick: () => void だけを定義すると、あとから「フォーカス時のイベントも欲しい」「id も付けたい」となったときに、無限に Props が増えていく「スイスチーズの穴」状態になる。
一意な解決策: HTML属性をすべて継承する
React の型定義を利用して、「標準のボタンが持っている機能はすべて持っている」 と宣言してしまおう。
TypeScript
// React.ComponentPropsWithoutRef を使うのが 2026年流のスマートなやり方
type ButtonProps = React.ComponentPropsWithoutRef<'button'> & {
variant?: 'primary' | 'secondary'; // 独自の拡張
};
export const Button = ({ variant = 'primary', ...props }: ButtonProps) => {
// ...props で残りの onClick や type, disabled などを一括で受け取る
return (
<button className={getMyStyle(variant)} {...props}>
{props.children}
</button>
);
};
2. 悩みの種: 「イベントハンドラの重複」
もしコンポーネント内部で onClick を使いたい(例:クリック音を鳴らす、ログを飛ばす)けれど、親からも onClick を渡したい場合、どちらかが上書きされて消えてしまうリスクがある。
一意な解決策: ハンドラの合成(Composition)
標準の onClick と独自のロジックを安全にマージする関数を用意しておくのがプロの技だ。
TypeScript
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log("内部ロジック実行!");
props.onClick?.(e); // 親から渡された onClick もしっかり呼ぶ
};
3. 悩みの種: 「ボタンじゃないボタン」への対応
以前話した通り、見た目はボタンだけど <a> タグとして動かしたい時、Props の型が button 用なのか a 用なのかでコンパイルエラーが起きる。
一意な解決策: asChild パターン
Radix UI が提唱したこのパターンは、Props の悩みを一気に解決する。
-
考え方: ボタン側で「タグ」を決め打たず、子要素にその役割を委譲(デリゲート)する。
-
メリット: 親は
<Button asChild>と書くだけで、中身が<a>だろうがLink (Next.js)だろうが、ボタンのスタイルを適用しつつ、正しいイベントを受け取れる。
4. 見落とされがちな視点: type="button" のデフォルト化
これは地味だけど、React のボタン設計で最も忘れられがちな「落とし穴」だ。
HTML の <button> は、デフォルトの type が "submit" なんだ。
-
悲劇: フォームの中で「キャンセルボタン」として作ったはずのコンポーネントをクリックしたら、ページがリロードされてフォームが送信されてしまった……。
-
対策: 汎用ボタンコンポーネントを作る時は、明示的に
type="button"をデフォルト値として設定しておくのが、一意に安全な設計だよ。
結論
ボタンの Props 設計に悩むのは、君が 「再利用性の高い、汎用的な部品」 を作ろうとしている証拠だね。
-
型は継承する:
ComponentPropsWithoutRefで楽をする。 -
スプレッド構文 (
...props) を使う: 独自の Props 以外はそのまま流す。 -
安全装置:
type="button"を忘れない。
これを意識するだけで、君のコンポーネントは「組み立てる楽しさ」を損なわない、最高に使い勝手の良い部品になるはずだ。
次は、「複数のイベント(ホバー、フォーカス、アクティブ)を考慮した、より高度なステート管理を含むボタン設計」 に挑戦してみる?
Would you like me to code a “Full-Featured Button” component that handles polymorphic tags, merging event handlers, and robust TypeScript definitions?