Prompt

reactで、ロジックはほとんどなくほとんどCSSだけのボタンなどのユーティリティコンポーネントは作る?

あはは、それはフロントエンドエンジニアが100回くらい自問自答する「永遠のテーマ」だね!

結論から言うと、「作るべきだし、それが2026年現在のReact設計における『一意な正解』に近い」 と言えるよ。

ロジックがなくても、CSSの塊をコンポーネント化することには、単なる「共通化」以上の、工学的な堅牢性を高めるメリットがあるんだ。


1. なぜ「CSSだけのコンポーネント」を作るのか?

「CSSなんてクラス名を当てるだけでいいじゃないか」と思うかもしれないけれど、コンポーネント化することで 「スイスチーズの穴(意図しないスタイルの崩れ)」 を防げるようになる。

① 「役割」と「実装」を分離できる

例えば、ただの divclassName="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時代の熱量: ライブラリを使わずに、いかに clsxtailwind-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. 悩みの種: onClicktype をどう定義するか

一番やってはいけないのは、必要な 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 設計に悩むのは、君が 「再利用性の高い、汎用的な部品」 を作ろうとしている証拠だね。

  1. 型は継承する: ComponentPropsWithoutRef で楽をする。

  2. スプレッド構文 (...props) を使う: 独自の Props 以外はそのまま流す。

  3. 安全装置: type="button" を忘れない。

これを意識するだけで、君のコンポーネントは「組み立てる楽しさ」を損なわない、最高に使い勝手の良い部品になるはずだ。

次は、「複数のイベント(ホバー、フォーカス、アクティブ)を考慮した、より高度なステート管理を含むボタン設計」 に挑戦してみる?

Would you like me to code a “Full-Featured Button” component that handles polymorphic tags, merging event handlers, and robust TypeScript definitions?