はじめに
日頃、Next.jsでTailwind CSSを使用していますが、最近ではcva、clsx、tailwind-mergeといったライブラリを使ったソースコードを多く見かけるようになりました。CSSの学習コストを抑えたいという気持ちもありますが、これらのライブラリの詳しい用途を知らないままでは効率的な開発が難しいとも思い、重い腰を上げて理解を深めるためにこの記事でまとめてみました。
この記事を読んで理解できるようになること(たぶん)
- cva、clsx、tailwind-mergeの用途
- 条件に応じたクラスの適用方法
- コンポーネントに任意のプロパティを設定し、スタイリングの種類を複数用意する方法
何のために用いるか
ユースケースとしては、自分たちのプロダクトで単一のコンポーネント(タグやボタンなど)に対して複数のスタイリングを適用させる必要がある場合を考えます。
FigmaなどのUI管理ツールと合わせると良いかと思います。
cvaとは
Class Variance Authority(cva)は、コンポーネントのバリエーションを簡単に定義するためのライブラリです。
clsxとは
clsxは、条件付きクラス名の管理を簡単にするためのライブラリです。
コンポーネントに渡されたpropsがtrueの場合にクラスが適用されるように使います。
tailwind-mergeとは
tailwind-mergeは、Tailwind CSSのクラスをマージしつつ、競合を解決するライブラリです。
今回は、複数のパターンで生成されるクラス名をマージしてくれます。
実装例
今回はボタンコンポーネントを例にします。
呼び出されるButtonコンポーネントの実装
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs))
}
import React, { FC, ReactNode } from "react";
import { cn } from "./utils";
import { cva } from "class-variance-authority";
interface ButtonProps {
children: ReactNode;
intent?: "primary" | "secondary" | "danger";
size?: "small" | "medium" | "large";
isDisabled?: boolean;
className?: string;
}
export const buttonVariants = cva("px-4 py-2 bg-blue-500 text-white rounded", {
variants: {
intent: {
primary: "bg-blue-500",
secondary: "bg-gray-400",
danger: "bg-red-400"
},
size: {
small: "px-4 py-2",
medium: "px-6 py-3",
large: "px-10 py-5"
},
},
defaultVariants: {
intent: "primary",
size: "medium",
},
});
const Button: FC<ButtonProps> = ({ children, intent = "primary", size = "medium", isDisabled = false, className }) => {
const buttonClass = cn(
buttonVariants({ intent, size }),
{ "btn-disabled": isDisabled },
className
);
return (
<button className={buttonClass} disabled={isDisabled}>
{children}
</button>
);
};
export default Button;
説明
ButtonコンポーネントはbuttonClassクラスを含むbutton要素を生成します。
buttonClassはbuttonVariants、btn-disabled、(Propsで渡ってくる)classNameの3種類をcn関数に渡しています。
cn関数ではclsxによって{ "btn-disabled": isDisabled }の部分の条件付きクラスの判別を行い、
その後twMergeによって渡ってきたTailwindのクラスを全てマージします。
なお、cn関数を用いているのは複数のサイトで同様の記法を目にしたのでそれを引用させてもらったものです。
わざわざutils.tsから呼び出さずとも、Buttonコンポーネント内でclsxとtwMergeを別々に呼び出しても構いません。
Buttonコンポーネントの呼び出し
<Button intent="primary" size="medium" isDisabled={false} className="">Click me</Button>
説明
上の実装に対する親からButtonコンポーネントを呼び出す方法です。
デフォルトでは以下のようなボタンが生成されます。
また、以下のようにisDisabled={true}とすることにより、条件付きクラスによってボタンの背景色が変わるとともに、button要素にdisabled=trueが渡されるため、ボタンが押せなくなります。
<Button intent="primary" size="medium" isDisabled={true} className="">Click me</Button>
さらに、Buttonコンポーネントを呼び出す際にclassName="bg-red-500"のように追加でクラスを適用させることで、デフォルトの設定をさらに上書きすることもできます。
<Button intent="primary" size="medium" isDisabled={false} className="bg-red-500">Click me</Button>
注意として、CSSは後で呼び出されたものを優先して適用することになっているため、以下の順番とする必要があリます。 propsで渡すclassNameをbuttonVariantsより前にしてしまうと、classNameによる上書きが効かなくなります。
const buttonClass = cn(
buttonVariants({ intent, size }),
{ "btn-disabled": isDisabled },
className
);
まとめ
一見すると自分と同じく可読性が低いなとネガティブな感想を持たれる方もいらっしゃるかと思いますが、多くのプロダクトはオリジナルのコンポーネント管理が必要になるため、そうなると今回のような実装は避けて通れないかと思います。
まだ使っていない方は、今後のために少しリソースを割いて取り組んでみてはいかがでしょうか。
※今回はSlotsについて触れておりませんが、これを活用するとより幅が広がるかと思いますので、必要に応じて取り上げたいと思います。