はじめに
shadcn/uiで導入されている下記ライブラリををキャッチアップしたので振り返る
- tailwind-merge
- class-variance-authority
- clsx
また、下記に当てはまる人は本記事を参考にしていただければと思います。
- MUIやChakraUIのようなUIライブラリを作りたい人
- もう少し使い勝手の良い、汎用的なカスタムコンポーネントを作りたい人
結論
まず、必要なライブラリをインストールします。
npm i tailwind-merge class-variance-authority clsx
次に、ユーティリティ関数を作成します。
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
そして、Buttonコンポーネントを作成します。
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を指定していないですか?
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>
}
レンダリングされるとbg-red-500 px-4 py-2 px-8
がclassNameに適応され、
CSSカスケードによってpx-8
は無視されます。
安全にスタイルを適応させる方法は、px-4
を削除すること。
ここで、tailwind-mergeが役立ちます。
npm i tailwind-merge
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ライブラリを作成する場合に便利です。
// MUI
<Button variant="outlined" color="primary">Outlined</Button>
// Chakra UI
<Button colorScheme='teal' size='lg' variant='ghost'>
Button
</Button>
npm i class-variance-authority
import { cva } from "class-variance-authority";
パッケージにエイリアスをつけることができ、インポート文を省略することができます。
v1以降から短い名前がデフォルトで使用できるようなので、気長に待ちましょう。
npm i cva@npm:class-variance-authority
import { cva } from "cva";
// 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を使うと、プロパティによって動的にスタイルを変更することができます。
npm install clsx
ButtonコンポーネントにisLoadedプロパティを追加し、isLoadedがtrueの場合にボタンの背景色を薄いグレーにするよう修正します。
export default function Page() {
return <Button className='px-8' isLoaded={true}>Save</Button>
}
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関数を定義します。
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
次に、cn関数を利用してButtonコンポーネントを修正します。
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)の活用法を紹介しました。間違いなどありましたらコメントにてご指摘ください、ここまで読んでいただきありがとうございました。
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
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>
)
}
参考