ライブラリに依存したコンポーネント開発から脱却するshadcn/ui

## Shadcn UI
[Shadcn UI](https://ui.shadcn.com)は[Radix UI](https://www.radix-ui.com/)と[Tailwind CSS](https://tailwindcss.com/)を用いたコンポーネントをプロジェクトに導入してくれるツールです。
ライブラリではなくツールとして活用できるのでプロジェクトと依存関係にはならず、作成したコンポーネントに関するあれこれをすべて手元の環境で行うところに利点があります。
このツールはNextjsやVite、RemixにAstroなど様々なプロジェクトで活用できます。
最近ではVercelが開発するAIがコンポーネントを生成してくれるv0.devの生成結果としても活用されています。

この記事ではNextjsを使う前提で記述しますが、本質的な部分は変わりません。

## 導入
Nextjsでプロジェクトを開始して、コンポーネントを作成します。

まずは`npx create-next-app`でアプリケーションの準備を行います。実行後の質問は以下のように答えました。Nextjsのバージョンは14.0.3です。
![スクリーンショット 2023-12-06 22.21.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/17c6e681-ac91-d82f-7f4d-948bb45ccc2c.png)
そして、`npx shadcn-ui@latest init`でShadcn UIの初期化を行います。
![スクリーンショット 2023-12-06 22.33.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/83f25fb3-036d-406a-0138-fb7d9a743250.png)
基本的には全部デフォルトで答えましたが、global CSSの場所を`src/app/globals.css`に、Tailwind CSSの設定ファイルの拡張子をtsに変更しました。

これによって`tailwind.config.ts`、`package.json`、`package.lock.json`、`src/app/globals.css`の更新が行われ、`components.json`、`src/lib/utils.ts`が作成されます。

`tailwind.config.ts`はJavaScriptでCommon JSの記法に書き直されます。そして、contentで記述される内容も少し雑に書き換えられるので修正するのが良いと考えています。他の変更は今後作るUIのため設定を行ってます。

`package.json`、`package-lock.json`では生成するUIで利用するパッケージの準備をします。`clsx`や`tailwind-merge`などが利用されます。

`src/app/globals.css`では生成するコンポーネントの色や角丸などのデザイントークンを生成されます。自由にカスタマイズして開発内容にあった内容に調整しましょう。

`components.json`はShadcn UIに関する設定が詰め込まれています。プロジェクト中で唯一Shadcn UIに関連するファイルとなります。このファイルが他と依存関係を結ぶことはないのでShadcn UIを利用しなくなったときはこのファイルを削除するだけで脱却できます。

`src/lib/utils.ts`はクラス名をつけるにあたって便利な関数を提供してくれます。`clsx`と`tailwind-merge`をいい感じに使えるようにしてくれています。

次に`src/app/layout.tsx`と`tailwind.config.ts`を触ってフォントの設定とShadcn UIのデフォルトのレイアウト設定を行います。
```tsx:src/app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { cn } from '@/lib/utils'

const inter = Inter({
  subsets: ['latin'],
  variable: "--font-sans"
})

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={cn(
          "min-h-screen bg-background font-sans antialiased",
          inter.variable
        )}>{children}</body>
    </html>
  )
}
```
フォントの`variables`化と`src/lib/utils.ts`に追加された`cn`を用いて基本的なスタイルを追加しています。
```ts:tailwind.config.ts
const { fontFamily } = require("tailwindcss/defaultTheme")

/** @type {import('tailwindcss').Config} */
module.exports = {
  // ...
  theme: {
    extend: {
      fontFamily: {
        sans: ["var(--font-sans)", ...fontFamily.sans],
      },
    },
  },
  // ...
}
```
tailwindの方にもフォントの設定を反映します。

これで環境の準備は完了です。`npx shadcn-ui@latest add button`のように実行することでボタンコンポーネントを`src/components/ui`に生成してくれます(CLIを用いない場合でも公式サイトで対象のコンポーネントのコードをコピーして利用できます)。

```tsx:src/components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline:
          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }
```
Nextjsの命名に則ってkebab-caseでファイルを作ってくれます。スタイリングはTailwind CSSをベースに`class-variance-authority`で変数ごとに出し分けしながら定義しています。そして、Radix UIの[`Slot`](https://www.radix-ui.com/primitives/docs/utilities/slot#slot)を用いて`asChild`を渡したときは要素を`children`に合わせるようなコンポーネントを定義しています。

## 設定ファイル
設定ファイルは初期化によって生成された`components.json`を指します。このファイルはCLIを用いたコンポーネント生成の際に用いられるファイルです。設定ファイルのJSON Schemaは[公式](https://ui.shadcn.com/schema.json)から提供されています。
スタイリングのベースとなる設定を行ったり、`tailwind`の設定ファイルを参照させたり、RSC・TSXの有無を設定したり、コンポーネントや便利関数をまとめるファイルの保存先を設定したりします。
基本的には初期化時に聞かれる内容が記述されているだけなので触ることはあまりないかもしれません。

## おわりに
Shadcn UIを紹介しました。多くのUIコンポーネントライブラリは利用するアプリケーションがそれ自体に依存してしまい移行などがネックとなってしまいます。Shadcn UIではこの点を払拭して安定したコンポーネント開発が行えるのでぜひ試してみてはいかがでしょうか。