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