最近流行りのshadcn/ui
に触れてみたのでその所感をまとめました。
ユースケース
- シンプルなデザインコンポーネントを使いたい(+少しだけ手を加えたい)
- プロジェクト内でコンポーネントコードを管理したい(NOTライブラリ)
- フロントエンドの実装にあまり時間使いたくない
検証環境
- Next.js v14
- Node.js v18
shadcn/ui とは?
簡単に言えば、カスタマイズ性に優れたUIコンポーネントの集まりです。
「シャドシーエヌ・ユーアイ」と呼びます。
現在Vercelに所属するエンジニアによって開発されてるそうです。
従来のコンポーネントライブラリと何が違うのか?
前提としてshadcn/ui
はコンポーネントライブラリではありません。
(公式でもこの点は強く否定しています)
This is NOT a component library. It's a collection of re-usable components that you can copy and paste into your apps.
What do you mean by not a component library?
I mean you do not install it as a dependency. It is not available or distributed via npm.
Pick the components you need. Copy and paste the code into your project and customize to your needs. The code is yours.
Use this as a reference to build your own component libraries.
押さえておくべき点としてshadcn/ui
は、本質的にはデザインシステムをコードとして宣言するメカニズムである、ということです。
どういうことか?
要するに、必要なコンポーネントがあれば都度あなたのプロジェクトに取り込むことができますよ、ということですね。
shadcn/ui は、 Radix UI と Tailwind CSS をベースに開発されており、柔軟性・カスタマイズ性 に定評があります。
従来のコンポーネントライブラリは概ね npmパッケージ として配布されておりそこで管理されますが、shadcn/uiはnpmの依存関係に影響しません。その代わりCLIを通してプロジェクトに直にコンポーネント(コード)を配置する仕組みをとります。
// MUIの場合
import Button from '@mui/material/Button';
// shadcn/uiの場合
import { Button } from "@/components/ui/button";
MUIではnode_modulesからコンポーネントを利用しますが、shadcn/uiはnpx shadcn-ui@latest add button
を実行後、プロジェクトのcomponents/ui
にbutton.tsx
が配置されます。利用する場合はそのコンポーネントを使用します。
なぜこの仕組みをとるのでしょうか?
その理由を作者は公式ページにて以下のように述べています。
Q. Why copy/paste and not packaged as a dependency?
A. The idea behind this is to give you ownership and control over the code, allowing you to decide how the components are built and styled.Start with some sensible defaults, then customize the components to your needs.One of the drawback of packaging the components in an npm package is that the style is coupled with the implementation. The design of your components should be separate from their implementation.
要約すると、
- コンポーネントコードの所有権はプロジェクトと開発者に与えたい
- ライブラリ依存のスタイルと実装の結びつきを解消させたい
という思想があるとのことです。
つまり、import ~ from @mui
のような形でコンポーネントライブラリとしてのインターフェースを持つのではなく、プロジェクトに直にコンポーネントを配置・コードを配布することで、ライブラリ依存での悩みの解決策を提供しますよ、ということですね。
メリット
依存関係を気にしない点とカスタマイズ性でしょうか。
依存関係を気にしない
フロントエンド周りを開発していると世に溢れる多くのUI・コンポーネントライブラリに遭遇します。その多くがnpm管理下でプロジェクトで使われているのではないでしょうか。
便利である一方、カスタマイズ時にライブラリ作法に則るよう制限がある、ドキュメントを漁ったりバージョンによっては最新で提供された機能が使用できないなど、局所的に欲しい機能のためだけにわざわざバージョンアップデートを検討したりもあって小さな改修で全体を見るような点も懸念点として考えられます。(あと2,3個使いたいだけなのに必要のない他全ても取り込むので重くなったりもしますよね)
shadcn/ui の場合、プロジェクトに直にコンポーネントを配置します。
CLIから直接コンポーネントを指定して配置するので大抵のコンポーネントはnpmの依存関係を気にする必要はありません。コンポーネントも必要なものだけプロジェクトに取り込みます。
- MUI
$ npm install @mui/material
> node_modules で管理
- shadcn/ui
$ npx shadcn-ui@latest add button
> Button コンポーネントを "src/components/ui" で管理
├── app
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── ui // shadcn/ui コンポーネント
│ │ ├── button.tsx
│ │ └── ...
カスタマイズ性
CLIで直接配置したコンポーネントは、Tailwind CSSで最低限の見た目と挙動がすでに記述されています。自前で一から実装する手間がないのでフロントエンドにあまり時間をかけたくない人には良いですね。tailwind.config.ts
でデフォルトのテーマを変えるだけでプロジェクトスタイルに合わせられることもサクッと調整できて良い点だと思います。
Button.tsx
import { Slot } from "@radix-ui/react-slot/dist";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
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 };
使ってみて良いなと思うのが、
使いにくくない丁度いい感じのところでコンポーネントを取得できるところですね。
DataTable
コンポーネントも丁度いい具合の挙動で取得できます。
コードを見ると部分的に分割された状態で実装されています。
この点も、「ちょっといじりたいな」と思った時に着手しやすい、といったメリットがあるかと。取り込みやすやだけでなくプロジェクトに合わせてカスタマイズしやすい点も評価が高いように思います。
(ライブラリ依存に関して世に評価されている shadcn/ui ですが、場合によってはライブラリを使用しています。例えばDataTableの場合、@tanstack/react-tableを使用しています。shadcn/ui自身はコンポーネントライブラリとしてのインターフェースを持ちません。が、世にある優れたライブラリが提供するコンポーネント群(TableやForm、Calendarなど)を使えないとなると開発者にとってストレスにつながります。そのため、必要なら関連する優秀なライブラリも取り込んでプロジェクトに配置しますよ、というスタンスをとっています。このような特徴もあって、cliを利用したデザインシステムによってコンポーネントを開発・利用できるツール=shadcn/uiと見る方が健全ですね)
DataTable.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
...
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
...
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
...
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
...
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
...
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
...
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
...
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
...
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
デメリット
デメリットというと大袈裟ですが、、
サクッとUIを作り、柔軟にカスタマイズしたい という開発者には shadcn/ui は非常に優れた選択肢かなと思う一方、考慮すべき点を挙げてみました。
コードの管理や保守
shadcn/ui はプロジェクトにコンポーネントコードを配置するので、コンポーネントの挙動に一貫性を保つための努力が必要となります。カスタマイズのしやすさと柔軟性が利点としてある一方で、構成や設計は検討する必要があります。
プロジェクトへの依存
shadcn/ui はTailwind CSS をスタイルで使用するためコンポーネントのスタイルがプロジェクトのテーマに依存します。そのため、他のプロジェクトへ移植するような利用が想定される際は運用上難しいでしょう。
利用して便利だなと感じたサービス
AI自動生成サービス
v0 というサービスでは shadcn/ui のコンポーネントを利用して自然言語からコードを生成してくれます。
- 基本的なものであれば簡単に作ってくれる
- 他人が作ったものを参考にできる
Figma
Figmaからコンポーネントを参照できます。
@shadcn/ui - Design System
(おまけ)アーキテクチャ
The anatomy of shadcn/ui という記事で shadcn/ui のアーキテクチャについてまとめてくれています。英語ですが構成とshadcn/ui が提供するコンポーネントの依存について解説してくれています。
まとめ
今回 shadcn/ui について所感をまとめました。
- ちょうど良い感じのコンポーネントを取得できる
- Tailwind CSSでベーススタイルを整えられる
という点で使ってみて良きかな、という感じですね。
個人的には shadcn/ui が提供する実装パターンやコンポーネント設計は学ぶべき点が多かったです。ライブラリ依存・UIカスタマイズに課題を感じる方であれば shadcn/ui は良い選択肢になるかもしれません。