【UIライブラリはもう不要？】TailwindCSSで汎用的なUIの作り方

# はじめに
shadcn/uiで導入されている下記ライブラリををキャッチアップしたので振り返る
- tailwind-merge
- class-variance-authority
- clsx
　　

また、下記に当てはまる人は本記事を参考にしていただければと思います。
- MUIやChakraUIのようなUIライブラリを作りたい人
- もう少し使い勝手の良い、汎用的なカスタムコンポーネントを作りたい人

# 結論
まず、必要なライブラリをインストールします。
```zsh
npm i tailwind-merge class-variance-authority clsx
```
次に、ユーティリティ関数を作成します。
```ts:lib/utils.ts
export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}
```
そして、Buttonコンポーネントを作成します。
```tsx:Button.tsx
export interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {
    children: React.ReactNode;
    isLoaded: boolean;
}

// 自分好みのvariantを設定
const buttonVariants = cva("rounded-md", {
    variants: {
        variant: {
            outlined: "bg-transparent text-blue-400 border",
            contained: [
                "bg-blue-400",
                "text-gray-800",
            ],
        },
        size: {
            sm: ["text-sm", "py-1", "px-2"],
            md: ["text-base", "py-2", "px-4"],
        },
    },
    defaultVariants: {
        variant: "outlined",
        size: "md",
    },
})

const Button = ({
    children,
    className,
    variant,
    size,
    isLoaded,
    ...props
}: ButtonProps) => {
    return (
        <button
            className={cn(buttonVariants({variant, size, className}), {
                // Propsの値によって適応するスタイルを指定
                "bg-gray-200": isLoaded,
            })}
            {...props}
        >
            {children}
        </button>
    )
}
```

# classNameの上書き　(tailwind-merge)
ベースとなるスタイルを指定して、Propsで背景色や文字色を変えたい。
そんなとき、こうやってclassNameを指定していないですか？
```tsx
export interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement> {
    children: React.ReactNode;
}

const Button = ({children, className, ...props}: ButtonProps) => {
    return (
        <button
            className={`bg-red-500 px-4 py-2 ${className}`}
            {...props}
        >
            {children}
        </button>
    )
}

export default function Page() {
    return <Button className='px-8'>Save</Button>
}
```
:::note warn
レンダリングされると`bg-red-500 px-4 py-2 px-8` がclassNameに適応され、
CSSカスケードによって`px-8`は無視されます。
:::

安全にスタイルを適応させる方法は、`px-4`を削除すること。
ここで、tailwind-mergeが役立ちます。

```zsh
npm i tailwind-merge
```

```tsx
export interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement> {
    children: React.ReactNode;
}

const Button = ({children, className, ...props}: ButtonProps) => {
    return (
        <button
            className={twMerge('bg-red-500 px-4 py-2', className)}
            {...props}
        >
            {children}
        </button>
    )
}

export default function Page() {
    return <Button className='px-8'>Save</Button>
}
```
これにより、被っているスタイルを削除され、安全にスタイルを適応できます。

# UIライブラリのようにvariantを作成する　(class-variance-authority)
`class-variance-authority`を使用することにより、型安全で`variant`や`size`などを設定できるようになります。
MUIやChakraUIのようなUIライブラリを作成する場合に便利です。
```tsx
// MUI
<Button variant="outlined" color="primary">Outlined</Button>
// Chakra UI
<Button colorScheme='teal' size='lg' variant='ghost'>
    Button
</Button>
```

```zsh
npm i class-variance-authority
```
```tsx
import { cva } from "class-variance-authority";
```

:::note
パッケージにエイリアスをつけることができ、インポート文を省略することができます。
v1以降から短い名前がデフォルトで使用できるようなので、気長に待ちましょう。

```zsh
npm i cva@npm:class-variance-authority
```

```tsx
import { cva } from "cva";
```
:::

```tsx
// VariantPropsでbuttonVariantsから型を抽出
export interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {
    children: React.ReactNode;
}

// cva(第一引数に共通で適応させたいスタイル, 第二引数にvariantを設定)
const buttonVariants = cva("rounded-md", {
    variants: {
        variant: {
            outlined: "bg-transparent text-blue-400 border",
            contained: [
                "bg-blue-400",
                "text-gray-800",
            ],
        },
        size: {
            sm: ["text-sm", "py-1", "px-2"],
            md: ["text-base", "py-2", "px-4"],
        },
    },
    // デフォルトで選択されるvariantを指定
    defaultVariants: {
        variant: "outlined",
        size: "md",
    },
})

const Button = ({children, className, variant, size ...props}: ButtonProps) => {
    return (
        <button
            className={twMerge(buttonVariants({variant, size, className}))}
            {...props}
        >
            {children}
        </button>
    )
}

export default function Page() {
    return <Button className='px-8'>Save</Button>
}
```

# propsによって動的にスタイルを変更したい (clsx)
clsxを使うと、プロパティによって動的にスタイルを変更することができます。
```zsh
npm install clsx
```
ButtonコンポーネントにisLoadedプロパティを追加し、isLoadedがtrueの場合にボタンの背景色を薄いグレーにするよう修正します。
```tsx
export default function Page() {
    return <Button className='px-8' isLoaded={true}>Save</Button>
}
```

```tsx
export interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {
    children: React.ReactNode;
    isLoaded: boolean;
}

const buttonVariants = cva("rounded-md", {...})

const Button = ({
    children,
    className,
    variant,
    size,
    isLoaded,
    ...props
}: ButtonProps) => {
    return (
        <button
            className={twMerge(
                clsx(buttonVariants({variant, size, className}), {
                    "bg-gray-200": isLoaded,
                })
            )}
            {...props}
        >
            {children}
        </button>
    )
}
```

# 扱いやすいようにcn関数を作成
cn関数を作成することで、CSSクラスの結合とマージを簡単に行えるようになります。以下は、TypeScriptでのユーティリティ関数cnとその使用例としてのButtonコンポーネントの実装です。
　
まず、clsxとtwMergeを使ったcn関数を定義します。
```ts
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

次に、cn関数を利用してButtonコンポーネントを修正します。
```tsx
const Button = ({
    children,
    className,
    variant,
    size,
    isLoaded,
    ...props
}: ButtonProps) => {
    return (
        <button
            className={cn(buttonVariants({variant, size, className}), {
                "bg-gray-200": isLoaded,
            })}
            {...props}
        >
            {children}
        </button>
    )
}
```

# まとめ
今回はshadcn/uiで使われているライブラリ（tailwind-merge, class-variance-authority, clsx）の活用法を紹介しました。間違いなどありましたらコメントにてご指摘ください、ここまで読んでいただきありがとうございました。

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

```tsx
export interface ButtonProps
    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {
    children: React.ReactNode;
    isLoaded: boolean;
}

const buttonVariants = cva("rounded-md", {
    variants: {
        variant: {
            outlined: "bg-transparent text-blue-400 border",
            contained: [
                "bg-blue-400",
                "text-gray-800",
            ],
        },
        size: {
            sm: ["text-sm", "py-1", "px-2"],
            md: ["text-base", "py-2", "px-4"],
        },
    },
    defaultVariants: {
        variant: "outlined",
        size: "md",
    },
})

const Button = ({
    children,
    className,
    variant,
    size,
    isLoaded,
    ...props
}: ButtonProps) => {
    return (
        <button
            className={cn(buttonVariants({variant, size, className}), {
                "bg-gray-200": isLoaded,
            })}
            {...props}
        >
            {children}
        </button>
    )
}
```

# 参考
https://github.com/dcastil/tailwind-merge

https://cva.style/docs

https://github.com/lukeed/clsx#readme