Not コンポーネントライブラリを謳う shadcn/ui というコンポーネント集について

2024/12/08に公開

こんにちは。@yyo616 です。
この記事は NTT Communications Advent Calendar 2024 の8日目の記事です。7日目の記事はCloud Workstations x Terraform で構築するフルマネージド開発環境です。

はじめに

フロントエンド開発において、コンポーネントライブラリやUIフレームワークの選定は、プロジェクト全体の生産性やメンテナンス性に大きく影響します。最近では、Tailwind CSSRadix UI と組み合わせて使用される shadcn/ui が注目を集めています。

shadcn/ui は多くのコンポーネントを提供している一方で、自身を This is NOT a component library. と紹介しています。これは一体どういうことでしょうか?

本記事では、shadcn/ui が従来のUIフレームワーク(UIライブラリ)とどのように異なり、なぜ注目を浴びているのかを明らかにした上で、その概要や利用方法、特徴について詳しく説明していきます。

UIフレームワークのトレンドの変遷(Bootstrap 〜 shadcn/ui)

まずは、shadcn/ui の独自性を理解しやすくするために、UIフレームワークのトレンドについて簡単に振り返っていきます。

Bootstrap

Webが一般化した2000年代後半から2010年代初頭、フロントエンドのデザインは手作業でCSSを書き込む作業が中心でした。各プロジェクトは独自のスタイルガイドラインや、行き当たりばったりのデザイン調整により、共通化や再利用性を実現するのが難しい時代でした。

そんな混沌とした時代に Bootstrap が登場しました。Bootstrapは統一されたクラス命名規則と、コンポーネント群をセットで提供することで、プロジェクト間でのデザイン・UI実装を大幅に効率化しました。

しかし、Bootstrapは包括的なデザインガイドラインを提供する代わりに、そのブランド色とも言える独特の「Bootstrap感」をプロジェクトに与えてしまいます。

UIライブラリ

Bootstrap以降、Google の Material Design を具現化した Material UI をはじめとした多数のUIライブラリが登場しました。これらはBootstrapよりも豊富なコンポーネントや、テーマ機能によるカスタマイズ性を重視しています。カラーパレットの変更やスタイルの上書きが容易になり、ブランドイメージに合わせた微調整がしやすくなりました。結果として、デザインの統一とカスタマイズ性の両立が進みました。

一方で以下のような課題が生まれました。

  • ランタイム依存:多くのUIライブラリは実行時にスタイルを計算・注入するため、パフォーマンスやビルド時間に悪影響を与えます。
  • ライブラリの依存関係:ライブラリが外部パッケージとして提供されるため、ライブラリ自身やその依存関係を定期的にアップデートする必要があります。
  • 実装上の制約:各ライブラリの思想に実装を合わせる必要があります。Bootstrap よりも柔軟性が上がったとはいえ、独自のデザインと完璧に整合させるのは難しいです。

Tailwind CSS / ヘッドレスUI

Tailwind CSS

先のような状況の中、Tailwind CSS が登場しました。「Utility First」を掲げ、ユーティリティクラスを使用することで事前にコンパイルされたスタイルを適用します。これにより、ランタイムでのスタイル生成が不要となり、パフォーマンスの向上とビルド時間の短縮が実現します。またデザイン上の制約が大幅に減少し、開発者は相当自由にUIを表現できるようになりました。

ヘッドレスUIライブラリ

Tailwind CSS の登場から少し経った時期に ヘッドレスUIライブラリ と呼ばれるライブラリも増えてきました。ヘッドレスUIは、コンポーネントの振る舞いのみに関与し、デザインは利用者が自由にカスタマイズできるというコンセプトです。代表的なヘッドレスUIライブラリとしては Radix UIHeadless UIReact Aria が挙げられます。

Tailwind CSS とヘッドレスUIライブラリは従来のUIライブラリと比べてメリットも多かった一方でデメリットも存在しました。

  • 開発コストの増大:スタイルの自由度と引き換えに従来のUIライブラリに比べて実装コストが生じます。使い回しや再利用性を保つためには独自のコンポーネントライブラリを内製するなど対応が必要になります。

shadcn/ui

Tailwind CSS とヘッドレス UI は独自のコンポーネントライブラリやデザインシステムを構築できるリソースがある企業にとっては非常にマッチします。しかしスタートアップや新規プロダクト開発のような十分なリソースの確保が難しい開発現場においては負担が大きいです。

そこに登場したのが shadcn/ui です。

shadcn/ui はTailwindやヘッドレスUIライブラリのメリットを享受しつつ、デメリットである実装コストも削減するために開発されたコンポーネント集です。昨今話題の v0 との関係性も相まって、shadcn/ui の存在感はますます強まっています。GitHubのスター数の推移を見ても、その注目度が伺えます。

shadcn/ui GitHub Stars

shadcn/ui とは?

shadcn/ui は、ヘッドレスUIライブラリの強みを活かしつつ、Tailwind CSSであらかじめスタイリングされたコンポーネント群を提供することで、フロントエンド開発の生産性と柔軟性を両立を目指しています。

https://ui.shadcn.com/

主な特徴は以下のとおりです。

  1. あらかじめスタイル済みのコンポーネント提供
    ボタン、モーダル、ドロップダウンなど、頻繁に利用するUIコンポーネントが既にスタイルされた状態で提供されます。これにより、デフォルトで洗練された見た目を活用可能です。

  2. Radix UIとの統合
    内部的にRadix UIを利用することで、標準的なアクセシビリティやキーボード操作のサポートを継承。開発者はUI/UX面での基本要件を満たしたコンポーネントを即座に活用できます。

  3. Tailwind CSSとの親和性
    Tailwind CSSが前提となっているため、提供されたコンポーネントをそのまま利用するほか、Tailwindのユーティリティクラスで細かなスタイル調整が容易に行えます。

  4. パッケージではなく「コピー&ペースト」での利用
    「コピー&ペースト」でコンポーネントをプロジェクト内に取り込む方針を取っています。あえてパッケージ化しないことでコードの所有権と制御権を利用者側に与えることができます。これがThis is NOT a component library.の所以です

これらの特徴により、「ヘッドレスUI + 自前でのスタイル設定」という初期作業を大幅に軽減でき、プロジェクト開始直後から一定水準のビジュアルを備えたUIを組み立てることが可能になります。

利用可能なコンポーネントの例

shadcn/ui は歴史の浅さに反して数多くのコンポーネントを提供しています。標準的なコンポーネントはもちろん、SonnerCollapsible などの珍しいコンポーネントまで用意されています。

また shadcn/ui は、Atomic Designでいうところの Templates に相当するような複雑なUIパターンを組み合わせたコンポーネントが用意しています。これにより、特定のレイアウトや機能を実現するUIを素早く構築できます。

たとえば、以下の例では、チャートを含む複合的なコンポーネントが多数用意されており、Tailwind CSS を用いて細やかなカスタマイズを行うことも可能です。

shadcn/ui chart examples

利用方法

shadcn/uiの導入手順はシンプルです。クイックに導入が可能です。

1. shadcn/ui の初期設定

shadcn/uiには、プロジェクト内で必要なコンポーネントをコピー&ペーストするためのコマンドラインツールが用意されています。これを利用してプロジェクトへのセットアップを行います。

npx shadcn@latest init

このコマンドを実行すると、いくつかの質問が表示されます。デフォルトオプションを指定してスムーズに進めたい場合は、以下のコマンドを使用できます。

npx shadcn@latest init -d

-d フラグにより、New York スタイルや Zinc カラーベース、CSS variables 使用有無といった設定が自動で行われます。手動で設定する場合は、プロンプトに沿って選択してください。

2. コンポーネントの追加

初期設定が完了したら、実際にコンポーネントを追加していきます。たとえば、Button コンポーネントを追加するには以下のコマンドを実行します。

npx shadcn@latest add button

これで以下のような 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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default:
          "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline:
          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    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 }

3. コンポーネントの使用

Button コンポーネントを使用するには、以下のようにインポートして利用します。

import { Button } from "@/components/ui/button"

export default function Home() {
  return (
    <div className="p-4">
      <Button onClick={() => alert("Clicked!")}>Click me</Button>
      <Button variant="outline" className="ml-2">
        Outline Button
      </Button>
    </div>
  )
}

気をつけたいところ

shadcn/ui は基本的に Radix UITailwind CSS に依存しており、軽量かつ拡張性の高いコンポーネントを即座に利用できます。しかし、リッチな機能を有するコンポーネントに関しては、内部的にサードパーティライブラリが利用されている場合があります。

例えば、Chart コンポーネントでは recharts のインストールが必要です。

Recharts Dependency

このように、単純にCLIコマンドで多くのコンポーネントを追加すると、依存関係が肥大化してしまう可能性があります。したがって、shadcn/ui のコンポーネントを導入する際には、以下の点を考慮しましょう。

  • 依存パッケージの確認
    コンポーネント追加前にドキュメントやソースコードを確認し、外部パッケージの必要性を把握しておくことで、後々のトラブルを軽減できます。

  • 本当に必要なコンポーネントのみ使用
    不要なコンポーネントをむやみに追加せず、プロジェクトに必要な機能に絞った活用を心がけることで、依存関係の複雑化を防ぎ、メンテナンス性を向上させることができます。

shadcn/ui は強力なツールですが、盲目的にコンポーネントを取り込むのではなく、必要性や依存関係を踏まえて慎重に利用することが重要です。

生成AIとの関係性

v0 との関係

昨今、Vercelが提供する v0 という生成UIツールが注目を集めています。v0 を通してUIコードを生成すると、デフォルトで shadcn/ui が使用され、TailwindベースのReactコードが生成されます。v0 が注目を集めることで、それに利用されている shadcn/ui も注目を集めるという関係性が出来上がっています。

Tailwindが選ばれる理由とshadcn/uiとの相性

text to code」領域には多くのツールが登場し、TailwindベースのUIコードを出力するパターンが増加しています。これは偶然ではなく、以下のような理由が考えられます。

  1. 広く浸透したエコシステム
    Tailwindはすでに多くのシェアを持ち、そのクラス名は開発者にとって共通言語となっています。

  2. CSSコード生成の不要性
    モデルはCSSのロジックまで生成する必要がなく、TSXやHTMLにTailwindクラスを付与するだけでスタイリングが可能になります。これはモデルの調整コストや複雑性を下げる効果があると言えます。

しかし、Tailwind が付与された HTML を生成するだけでは、現場で求められる「再利用可能なコンポーネント指向」の水準には届きません。エンジニアは既存のコンポーネントをベースにした高レベルなコードを求めています。そこで重要になるのが、shadcn/ui のような存在です。

shadcn/ui は、柔軟なスタイリングを可能にするTailwindを活用しつつ、Radix UIによるアクセシビリティやロジックを内包したコンポーネント群を用意しています。生成するコード品質を向上させるために、モデルの知識として shadcn/ui を利用していることは想像に難くないです。

所感

今回紹介した shadcn/ui は、成長段階のスタートアップや新規プロダクト開発フェーズにおいて抜群に相性が良い印象です。まずは基本品質を担保しつつプロダクトを素早く構築し、PMF(Product-Market Fit) を達成したら段階的にデザインシステムを構築していくというフローにおいて、shadcn/ui の採用による恩恵は非常に大きいと感じます。

私自身も新規プロダクトを開発中であり、普段は shadcn/ui を利用したフロントエンドの開発も行っています。現状では開発速度を担保しつつも、将来的な技術負債やデザイン負債を抑制できていると感じており、開発者体験は概ね良好です。

shadcn/ui が今後も伸び続けるかどうかは予想が難しいですが、昨今のフロントエンドの状況を踏まえると、shadcn/ui のようなヘッドレスUIライブラリと従来的なUIライブラリの中間を担うようなUIフレームワーク自体は今後も増えていくと思っています。

参考

https://ui.shadcn.com/

https://speakerdeck.com/azukiazusa1/ui-kai-fa-niokeru-hetudoresu-ui-raiburarinozhong-yao-xing-todezainsisutemuhenoqu-riru-refang

https://note.exawizards.com/n/n3ca419a4b81a

Discussion