ぼくがおすすめするButtonコンポーネント構成【Next.js, Tailwind, TypeScript】

# 前置き
ここでご紹介するButtonコンポーネントの構成はあくまで複数あるであろうベストプラクティスの一つとして存在できたらいいなと思っています。
そのため、考慮漏れなどがあるかもしれませんので、その際はコメントなどでご指摘いただけると幸いです！

![](https://storage.googleapis.com/zenn-user-upload/0c82a46c3b1f-20231211.png)

# この記事で知れること
- 複数のスタイルに対応できるButtonコンポーネントの作り方

# 結論
僕はいつも下記のようなButtonコンポーネントを用意し、プロジェクトで使いまわしています。

```ts:Button.tsx
import React from 'react';
import { cn } from '@/libs/utils';
import { VariantProps, cva } from 'class-variance-authority';

export const buttonVariants = cva(
  `flex h-10 items-center justify-center gap-2 rounded-full px-4 text-textL_medium transition disabled:opacity-50 disabled:pointer-events-none transition`,
  {
    variants: {
      variant: {
        primary: `bg-accent text-primaryWhite hover:opacity-75`,
        outline: `bg-white text-accent border border-accent hover:opacity-75`,
        icon: `bg-transparent hover:bg-bg-gray rounded-full p-2 h-fit`,
      },
    },
    defaultVariants: {
      variant: 'primary',
    },
  }
);

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (
    { className, variant, children, disabled, type = 'button', ...props },
    ref
  ) => {
    return (
      <button
        type={type}
        className={cn(buttonVariants({ variant, className }))}
        ref={ref}
        disabled={disabled}
        {...props}
      >
        {children}
      </button>
    );
  }
);

Button.displayName = 'Button';
```

```ts:utils.ts
import { twMerge } from 'tailwind-merge';
import { type ClassValue, clsx } from 'clsx';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

# 実際の使い方
このコードでは、一つのButtonコンポーネントからプライマリー、セカンダリー、アイコンの3種類のボタンを生成しています。

```ts:page.tsx
import { Button } from '@/components/Button/Button';
import { HiPlus } from 'react-icons/hi';

export default function HomePage() {
  return (
    <div>
      <div className="m-14 flex w-fit flex-col gap-4">
        {/* Primary */}
        <Button>プライマリーボタン</Button>

        {/* Secondary */}
        <Button variant="outline">セカンダリーボタン</Button>

        {/* Icon */}
        <Button variant="icon" className="h-10 w-10 p-0">
          <HiPlus size={32} />
        </Button>
      </div>
    </div>
  );
}
```
![](https://storage.googleapis.com/zenn-user-upload/0c82a46c3b1f-20231211.png)

# コンポーネントの解説
このButtonコンポーネントで意識していることは、一つのコンポーネントで複数のスタイルに対応できるということです。
これを実現するために、`utils.ts`で`cn`というユーティリティ関数を別途作成し、Buttonコンポーネントに適用しています。
Buttonコンポーネントで`variant`を指定することで、対応したスタイルがボタンに適用され、複数の用途で使用することができるようになります。

`buttonVariants`で`variants`を指定することができます。もし他の`variant`を追加したい場合は、ここにスタイルを追加するだけでButtonコンポーネントにスタイルを適用できるようになります。

```ts:Button.tsx
export const buttonVariants = cva(
// ベースのボタンスタイル
  `flex h-10 items-center justify-center gap-2 rounded-full px-4 text-textL_medium transition disabled:opacity-50 disabled:pointer-events-none transition`,
  {
    variants: {
      variant: {
      // 各種ボタンスタイル
        primary: `bg-accent text-primaryWhite hover:opacity-75`,
        outline: `bg-white text-accent border border-accent hover:opacity-75`,
        icon: `bg-transparent hover:bg-bg-gray rounded-full p-2 h-fit`,
      },
    },
    defaultVariants: {
    // デフォルトのボタンスタイル
      variant: 'primary',
    },
  }
);
```

`utils.ts`で定義した`cn`は、`button`タグにclassNameを適用する際に使用しており、こうすることでTailwindのクラス名をいい感じに結合してくれる`twMerge`と、classNameを動的に生成する時に便利な`clsx`の両方の恩恵を受けられるようにしています。
```ts:Button.ts
<button
        type={type}
        className={cn(buttonVariants({ variant, className }))}
        ref={ref}
        disabled={disabled}
        {...props}
      >
        {children}
      </button>
```

# インラインスタイル的なカスタマイズ
下記コードのように、ButtonコンポーネントにclassNameを適用することができ、指定したスタイルで上書きをすることができます。そのため、一部のボタンのサイズを変えたいなどの要望にも柔軟に対応できるような構成になっています。
```ts:page.tsx
{/* Icon */}
<Button variant="icon" className="h-10 w-10 p-0">
  <HiPlus size={32} />
</Button>
```

# まとめ
個人的に、Next.js、Tailwind、TypeScriptで開発を行うことが多いため、この技術スタックを使う時におすすめのButtonコンポーネントの作り方ということでご紹介しました。
もし「こうしたらさらに使い勝手良くならない？」などのご意見があったらどんどんください！
Buttonコンポーネントの最適解をアップデートしましょう。