🐷

React + Tailwind CSS でコンポーネントを改善してみた

に公開

はじめに

以前、以下の記事でReact + Tailwindを使ったシンプルなボタンコンポーネントの実装について紹介しました:

👉 React + Tailwind CSS でコンポーネントを作ってみた

当時の実装はPropsベースで状態を切り替える基本的なものでしたが、以下のような問題点がありました。

❌ 旧実装の課題点

  • スタイルの定義がハードコードで、型安全性が弱い

  • クラス名が文字列連結で可読性・保守性が低い

  • onClick の型が any でTypeScriptの恩恵が薄い

  • ref や as による柔軟なレンダリングができない

  • Composition(アイコン + テキスト)パターンに非対応

  • アクセシビリティやカスタマイズ性が不足

このように、学習用には十分ですが、拡張性・保守性・再利用性という点でプロダクションレベルには不向きでした。

そこで今回は、tailwind-variants + Polymorphic Design + Composition Pattern を組み合わせ、実務レベルで強いButtonコンポーネントを構築してみます。


なぜ従来のProps方式を脱却するか?

以下のような典型的なProps方式のButtonは、初学者向けには良いですが、柔軟性と保守性に欠けます。

<Button color="blue" size="lg" rounded block>
  Submit
</Button>
  • propsの組み合わせが増えると管理が困難(
  • サイズや色の変更が困難
  • ボタン以外(aタグ、divなど)への拡張性がない

tailwind-variantsで状態管理を抽象化する

tailwind-variants を使えば、複数のバリエーション(サイズ・色・丸みなど)を一元的に安全に定義できます。

import { tv } from "tailwind-variants";

export const button = tv({
  base: "inline-flex items-center justify-center font-semibold focus:outline-none transition disabled:opacity-50 disabled:pointer-events-none",
  variants: {
    color: {
      blue: "bg-blue-600 text-white hover:bg-blue-700",
      dark: "bg-black text-white hover:bg-gray-800",
      ghost: "bg-transparent hover:bg-gray-100 text-black",
    },
    size: {
      sm: "px-3 py-1 text-sm",
      md: "px-4 py-2 text-base",
      lg: "px-5 py-3 text-lg",
    },
    rounded: {
      none: "rounded-none",
      sm: "rounded-sm",
      md: "rounded-md",
      full: "rounded-full",
    },
    block: {
      true: "w-full",
    },
  },
  defaultVariants: {
    color: "blue",
    size: "md",
    rounded: "md",
    block: false,
  },
});

Polymorphic Component(as対応)で柔軟性UP

以下のように、button, a, divなど任意のタグとして使えるようにしておくと、UIの汎用性が高まります。

import { forwardRef, ElementType, ReactNode } from "react";
import clsx from "clsx";
import { button } from "./utils/buttonStyles";

import type { VariantProps } from "tailwind-variants";

type ButtonVariants = VariantProps<typeof button>;

type ButtonProps<C extends ElementType = "button"> = {
  as?: C;
  children: ReactNode;
  className?: string;
} & ButtonVariants &
  Omit<React.ComponentPropsWithoutRef<C>, "as" | "size" | "color">;

const Button = forwardRef(
  <C extends ElementType = "button">(
    {
      as,
      children,
      className,
      size,
      color,
      rounded,
      block,
      ...rest
    }: ButtonProps<C>,
    ref: React.Ref<Element>
  ) => {
    const Component = as ?? "button";
    return (
      <Component
        ref={ref}
        className={clsx(button({ size, color, rounded, block }), className)}
        {...rest}
      >
        {children}
      </Component>
    );
  }
);

Button.displayName = "Button";
export default Button;

Composition Patternを取り入れる

複雑なUIを想定するなら、以下のようにスロット的に Button.Icon, Button.Text を導入しておくのも有効です(ここでは省略)。

<Button>
  <Button.Icon />
  <Button.Text>保存</Button.Text>
</Button>

使い方の例

<Button color="blue" size="lg">
  通常のボタン
</Button>

<Button as="a" href="https://zenn.dev" color="ghost">
  リンクとして使う
</Button>

<Button block rounded="full">
  フル幅のボタン
</Button>

アクセシビリティ配慮

  • aria-* 属性などは明示的に追加可能
  • disabled は適切に透過される
  • スクリーンリーダー対応に必要な設計も容易に追加できる

終わりに

こうした設計をベースにしておくことで、チーム開発・デザインシステム・アクセシビリティ・テスト性・将来的なスケーラビリティのすべてにおいて強いコンポーネントが実現できます。

UIライブラリに頼らず、自前でコンポーネントを洗練させていく力は今後ますます重要になります。

Discussion