1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-05-06

shadcn/ui とは

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

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

利用方法

1. 初期セットアップ

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

npx shadcn-ui@latest init

2. コンポーネント追加

Card コンポーネントを追加してみます。
shadcn/ui のドキュメントを参照し、「Installation」に記載してあるコマンドを実行します。

npx shadcn@latest add card

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

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

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

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

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 の設定方法や使い方についてはこちらの記事をご参照ください。

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

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 コンポーネントのドキュメントの「Usage」を参考に、コンポーネントを作っていきます。

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

npm install date-fns@^4

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

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

@/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. 記事一覧ページから呼び出す

@/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

(スマホサイズ)
image-2.png

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

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

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

@/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

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

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

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 コンポーネントを手軽に導入でき、美しいデザインを素早く整えたい場面で非常に有用です。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?