Next.js + shadcn/ui でモダンなUIを構築する手順と実践例

## shadcn/ui とは

shadcn/ui は、React + Tailwind CSS を使った再利用可能なコンポーネント集です。
開発者が UI を素早く整えつつ、柔軟にカスタマイズできるように設計されています。
「UI コンポーネントライブラリ」ではなく、「コピーしてカスタマイズする前提のテンプレート集」という考え方です。

| 特徴                   | 説明                                                                                  |
| ---------------------- | ------------------------------------------------------------------------------------- |
| **Tailwind CSS**ベース | Tailwind でスタイルを完全にカスタマイズ可能。独自のデザインシステムに柔軟に対応。     |
| **Radix UI 利用**      | アクセシビリティが高く、UI の動作が安定している。モーダル、ドロップダウンなどに強い。 |
| **CLI で導入可能**     | CLI でコンポーネントを簡単に追加できる。                                              |
| **オープンソース**     | コンポーネントのコードが全て見える＆編集可能で、プロジェクトに完全に組み込める。      |
| **テーマ対応**         | ダークモードやカスタムカラーなど、テーマの切り替えがしやすい。                        |

## 利用方法

### 1. 初期セットアップ

プロジェクト直下で以下のコマンドを実行します。

```bash
npx shadcn-ui@latest init
```

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

Card コンポーネントを追加してみます。
[shadcn/ui のドキュメント](https://ui.shadcn.com/docs/components/card)を参照し、「Installation」に記載してあるコマンドを実行します。

```bash
npx shadcn@latest add card
```

実行すると `/components/ui/card.tsx` が生成されます。

## demo: Card コンポーネントで記事一覧ページ作成

ブログサイトの記事一覧ページを、Card コンポーネントを利用して作成します。

プロジェクトは Next.js/TypeScript + Prisma で構築します。

```bash
my-project/
├── prisma/
│   ├── schema.prisma
│   └── seed.cjs
└── src/
    ├── app/
    │   ├── page.tsx
    │   └── contents/
    │       └── [id]/
    │           └── page.tsx
    ├── components/
    │   ├── post/
    │   │   └── list.tsx
    │   └── ui/
    │       └── card.tsx
    └── lib/
        └── prisma.ts
```

#### 1. Prisma モデルの設定

Prisma の設定方法や使い方についてはこちらの記事をご参照ください。

https://qiita.com/m-bitterC2/items/63607378efb0cedcabcd

スキーマ定義は以下の通りです。

```: schema.prisma
model User {
  id        String    @id @default(cuid())
  userName  String
  posts     Post[]
  createdAt DateTime  @default(now())
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String
  createdAt DateTime @default(now())
  authorId  String
  author    User     @relation(fields: [authorId], references: [id])
}
```

#### 2. Card コンポーネントでコンテンツを作成

[Card コンポーネントのドキュメント](https://ui.shadcn.com/docs/components/card)の「Usage」を参考に、コンポーネントを作っていきます。

まず、日付フォーマットで使用するライブラリとして、`date-fns` をインストールしておきます。

```bash
npm install date-fns@^4
```

記事一覧ページから呼び出すコンポーネントを作成します。

:::note info
本題から少し外れますが、今回は Next.js 15 の App Router を使っているため、画面遷移用フック「useRouter」を 'next/navigation' からインポートします。
'next/router' にも useRouter は存在しますが、こちらは Page Router 用のフックです。
:::

```tsx: @/components/post/list.tsx
import { formatDistanceToNow } from "date-fns";
import { ja } from "date-fns/locale";
import { useRouter } from "next/navigation";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

type Props = {
  title: string;
  id: string;
  createdAt: Date;
  content: string;
  authorId: string;
  author: {
    id: string;
    userName: string;
    createdAt: Date;
  };
};

const PostList = (post: Props) => {
  const router = useRouter();

  return (
    <Card
      className="cursor-pointer hover:shadow-lg transition-shadow"
      onClick={() => router.push(`/contents/${post.id}`)}
    >
      <CardHeader>
        <CardTitle>{post.title}</CardTitle>
      </CardHeader>

      <CardContent>
        <p className="text-sm mb-2 line-clamp-3">{post.content}</p>

        <div className="flex items-center justify-between text-sm text-gray-300">
          <span>{post.author.userName}</span>
          <time>
            {/* 「5分前」「2日前」などの相対時刻を日本語で表示 */}
            {formatDistanceToNow(new Date(post.createdAt), {
              addSuffix: true,
              locale: ja,
            })}
          </time>
        </div>
      </CardContent>
    </Card>
  );
};

export { PostList };
```

#### 3. 記事一覧ページから呼び出す

```tsx: @/app/page.tsx
import { PostList } from "@/components/post/list";
import { prisma } from "@/lib/prisma";

export default async function Home() {
  const posts = await prisma.post.findMany({
    orderBy: { createdAt: "desc" },
    include: { author: true },
  });

  return (
    <div className="container mx-auto px-5 py-10">
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {posts.map((post) => (
          <PostList key={post.id} {...post} />
        ))}
      </div>
    </div>
  );
}
```

ここまでの実装で、Card コンポーネントで作成したコンテンツを表示できます。

(PC サイズ)
![image-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665246/1999894e-3c97-4524-867e-7d407b2736bc.png)


(スマホサイズ)
![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665246/70a5eb57-2a39-4ecb-95cc-dfc0399dc6f9.png)


#### 4. コンテンツをクリックして画面遷移

Card コンポーネントに `useRouter` を設定することで、コンテンツをクリックすると `contents/[id]` ページに遷移させることができます。

Card コンポーネントの遷移機能は実装済みなので、あとは遷移先のページを作成しておきます。

```tsx: @/app/contents/[id]/page.tsx
import { format } from "date-fns";
import { ja } from "date-fns/locale";
import { prisma } from "@/lib/prisma";

type Params = {
  params: Promise<{ id: string }>;
};

const page = async ({ params }: Params) => {
  const { id } = await params;

  const post = await prisma.post.findUnique({
    where: { id },
    include: { author: true },
  });

  return (
    <div className="container mx-auto px-8 py-10">
      <div className="flex justify-between items-center mb-4">
        <p className="text-sm text-gray-500">投稿者: {post?.author.userName}</p>
        <time className="text-sm text-gray-500">
          {format(new Date(post?.createdAt ?? 0), "yyyy年MM月dd日", {
            locale: ja,
          })}
        </time>
      </div>
      <h1>{post?.title}</h1>
      <br />
      <p>{post?.content}</p>
    </div>
  );
};

export default page;
```

ここまでの実装で、一覧ページのコンテンツをクリックすると、ID に紐づく情報が取得されたページに遷移できます。

![image-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3665246/92b32f95-323d-411f-9b6f-8dad6dc89042.png)


:::note info
ちなみに、Next.js 15 ではリンク先を事前に取得して画面遷移を高速化できる Link コンポーネントが用意されています。
こちらは、RSC・RCC の両方で使用できます。

今回使用した useRouter による画面遷移では、Link コンポーネントのようなプリフェッチ機能はありません。

```tsx
import Link from "next/link";

const page = () => {
  return (
    <div>
      <p>サーバーコンポーネント</p>
      <Link href={"/about"}>Aboutページへ移動</Link>
    </div>
  );
};

export default page;
```

:::

## まとめ

本記事では、shadcn/ui の概要や特徴、実際の利用方法についてデモを交えて紹介しました。
Tailwind CSS と Radix UI を活用した高品質な UI コンポーネントを手軽に導入でき、美しいデザインを素早く整えたい場面で非常に有用です。