0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-07-07

はじめに

shadcn/uiで導入されている下記ライブラリををキャッチアップしたので振り返る

  • tailwind-merge
  • class-variance-authority
  • clsx
      

また、下記に当てはまる人は本記事を参考にしていただければと思います。

  • MUIやChakraUIのようなUIライブラリを作りたい人
  • もう少し使い勝手の良い、汎用的なカスタムコンポーネントを作りたい人

結論

まず、必要なライブラリをインストールします。

npm i tailwind-merge class-variance-authority clsx

次に、ユーティリティ関数を作成します。

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

そして、Buttonコンポーネントを作成します。

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を指定していないですか?

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を使用することにより、型安全でvariantsizeなどを設定できるようになります。
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>
    )
}

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?