🎉

shadcn/uiで新しくリリースされたCarouselを触ってみる

2023/12/31に公開

はじめに

shadcn/ui に新しいコンポーネントが追加されました。追加されたコンポーネントは以下の通りとなります。

この記事では追加された Carousel コンポーネントを触ってみます。

以下は作者である@shadcnのツイートです。

https://twitter.com/shadcn/status/1738283285032181931

この記事の対象者

  • Next.js で開発する方
  • shadcn/ui に興味があるかた
  • shadcn/ui で追加された Carousel に興味ある方

忙しい人向けの記事のまとめ

忙しい人向けに、ポイントをまとめておきます。

shadcn/ui は、@shadcnが開発しているコンポーネントライブラリーです。特徴としてパッケージのインストールは不要で、コンポーネントはコピー&ペーストするだけで使うことができます。自身のニーズにあわせてデザインをカスタマイズできます。

https://ui.shadcn.com/

shadcn/ui を利用することで以下のような Carousel を作成できます。

Alt text

shadcn/ui の Carousel は裏側としてはEmbla Carouselをヘッドレスとして利用しているようです。Embla Carousel をより理解することで、より高度なカスタマイズが可能になります。

https://www.embla-carousel.com/get-started/react/

本記事では上記の Carousel を作成するための手順を解説します。作業リポジトリは以下にあります。

https://github.com/hayato94087/next-shadcnui-sample

以降では、詳細な手順を解説します。

Next.js プロジェクトの新規作成

作業するプロジェクトを新規に作成していきます。

長いので、折り畳んでおきます。

新規プロジェクト作成と初期環境構築の手順詳細

プロジェクトを作成

$ pnpm create next-app@latest next-shadcnui-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd next-shadcnui-sample

Peer Dependenciesの警告を解消

Peer dependenciesの警告が出ている場合は、pnpm installを実行し、警告を解消します。

 WARN  Issues with peer dependencies found
.
├─┬ autoprefixer 10.0.1
│ └── ✕ unmet peer postcss@^8.1.0: found 8.0.0
├─┬ tailwindcss 3.3.0
│ ├── ✕ unmet peer postcss@^8.0.9: found 8.0.0
│ ├─┬ postcss-js 4.0.1
│ │ └── ✕ unmet peer postcss@^8.4.21: found 8.0.0
│ ├─┬ postcss-load-config 3.1.4
│ │ └── ✕ unmet peer postcss@>=8.0.9: found 8.0.0
│ └─┬ postcss-nested 6.0.0
│   └── ✕ unmet peer postcss@^8.2.14: found 8.0.0
└─┬ next 14.0.4
  ├── ✕ unmet peer react@^18.2.0: found 18.0.0
  └── ✕ unmet peer react-dom@^18.2.0: found 18.0.0
$ pnpm i postcss@latest react@^18.2.0 react-dom@^18.2.0

これで警告が解消されました。

クリーンアップ

不要な設定を削除し、プロジェクトを初期化します。

styles

CSSなどを管理するstylesディレクトリを作成します。globals.cssを移動します。

$ mkdir src/styles
$ mv src/app/globals.css src/styles/globals.css

globals.cssの内容を以下のように上書きします。

src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

初期ページ

app/page.tsxを上書きします。

src/app/page.tsx
import { type FC } from "react";

const Home: FC = () => {
  return (
    <div className="">
      <div className="text-lg font-bold">Home</div>
      <div>
        <span className="text-blue-500">Hello</span>
        <span className="text-red-500">World</span>
      </div>
    </div>
  );
};

export default Home;

レイアウト

app/layout.tsxを上書きします。

src/app/layout.tsx
import "@/styles/globals.css";
import { type FC } from "react";
type RootLayoutProps = {
  children: React.ReactNode;
};

export const metadata = {
  title: "Sample",
  description: "Generated by create next app",
};

const RootLayout: FC<RootLayoutProps> = (props) => {
  return (
    <html lang="ja">
      <body className="">{props.children}</body>
    </html>
  );
};

export default RootLayout;

TailwindCSSの設定

TailwindCSSの設定を上書きします。

tailwind.config.ts
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  plugins: [],
}
export default config

TypeScriptの設定

baseUrlを追加します。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
+   "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

動作確認

ローカルで動作確認します。

$ pnpm dev

コミット

コミットして作業結果を保存しておきます。

$ pnpm build
$ git add .
$ git commit -m "新規にプロジェクトを作成し, 作業環境を構築"

shadcn/ui

ここでは shadcn/ui を設定します。

shadcn/uiとは

shadcn/ui は、@shadcnが開発しているコンポーネントライブラリーです。コンポーネントはアプリケーションにコピー&ペーストして使うことができます。

主な特徴

  • 美しいデザイン: コンポーネントは見た目が良く、現代的な UI のニーズに適応しています。
  • 簡単な統合: コンポーネントはコピー&ペーストするだけで使うことができ、迅速にプロジェクトに統合可能です。
  • アクセシビリティ: すべてのユーザーがアクセスしやすいように設計されています。
  • カスタマイズ可能: さまざまなニーズに合わせてカスタマイズできます。
  • オープンソース: GitHub でソースコードが公開されており、コミュニティによる貢献が可能です。

https://ui.shadcn.com/

shadcn/uiを導入

shadcn/ui を設定します。

$ pnpm dlx shadcn-ui@latest init
✔ Would you like to use TypeScript (recommended)? … no / yes
✔ Which style would you like to use? › Default
✔ Which color would you like to use as base color? › Slate
✔ Where is your global CSS file? … src/styles/globals.css
✔ Would you like to use CSS variables for colors? … no / yes
✔ Are you using a custom tailwind prefix eg. tw-? (Leave blank if not) … 
✔ Where is your tailwind.config.js located? … tailwind.config.ts
✔ Configure the import alias for components: … @/components
✔ Configure the import alias for utils: … @/lib/utils
✔ Are you using React Server Components? … no / yes
✔ Write configuration to components.json. Proceed? … yes

✔ Writing components.json...
✔ Initializing project...
✔ Installing dependencies...

Success! Project initialization completed.

作業結果を保存しておきます。

$ pnpm build
$ git add .
$ git commit -m "shadcn/uiを設定"

Carousel を試してみます。

Carousel のドキュメントは以下にあります。

https://ui.shadcn.com/docs/components/carousel

Carousel がヘッドレスとして利用している Embla Carousel のドキュメントと API リファレンスは以下になります。

https://www.embla-carousel.com/get-started/react/

https://www.embla-carousel.com/api/

コンポーネント/パッケージの追加

Carousel の実装に必要な shadcn/ui のコンポーネントとパッケージを追加します。embla-carousel-autoplay はオートプレイ時に必要なパッケージです。

$ pnpm dlx shadcn-ui@latest add carousel card
$ pnpm add embla-carousel-autoplay

src/components/ui/carousel.tsx が追加されました。ソースコードを見ると、Carousel コンポーネントはEmbla Carouselをヘッドレスとして利用しているようです。

src/components/ui/carousel.tsx
import useEmblaCarousel, {
  type EmblaCarouselType as CarouselApi,
  type EmblaOptionsType as CarouselOptions,
  type EmblaPluginType as CarouselPlugin,
} from "embla-carousel-react"

https://www.embla-carousel.com/get-started/react/

実装

Carousel コンポーネントを利用するため専用の page.tsx と Carousel を実装する carousel.tsx を作成します。

$ mkdir src/app/carousel
$ touch src/app/carousel/page.tsx
$ touch src/components/carousel.tsx
src/app/carousel/page.tsx
import {
  Carousel1,
  Carousel2,
  Carousel3,
  Carousel4,
  Carousel5,
  Carousel6,
  Carousel7,
} from "@/components/carousel";
import { FC } from "react";

const CarouselPage: FC = () => {
  return (
    <div className="pt-20 pb-20 bg-gray-200">
      <div className="flex flex-col items-center justify-items-center space-y-20">
        <div className="flex flex-col space-y-3">
          <span className="text-sm font-bold self-center">
            表示数:1, ループ:あり, 表示方向:横, オートプレイ:なし
          </span>
          <Carousel1 />
        </div>
        <div className="flex flex-col space-y-3">
          <span className="text-sm font-bold self-center">
            表示数:2, ループ:なし, 表示方向:横, オートプレイ:なし
          </span>
          <Carousel2 />
        </div>
        <div className="flex flex-col space-y-3">
          <span className="text-sm font-bold self-center">
            表示数:3, ループ:なし, 表示方向:横, オートプレイ:なし
          </span>
          <Carousel3 />
        </div>
        <div className="flex flex-col space-y-3">
          <span className="text-sm font-bold self-center">
            表示数:3, ループ:あり, 表示方向:横, オートプレイ:なし
          </span>
          <Carousel4 />
        </div>
        <div className="flex flex-col space-y-3">
          <span className="text-sm font-bold self-center">
            表示数:3, ループ:あり, 表示方向:横, オートプレイ:あり
          </span>
          <Carousel5 />
        </div>
        <div className="flex flex-col space-y-3 items-center">
          <span className="text-sm font-bold self-center">
            表示数:レスポンシブ, ループ:なし, 表示方向:横, オートプレイ:なし
          </span>
          <Carousel6 />
        </div>
        <div className="flex flex-col space-y-16">
          <span className="text-sm font-bold self-center">
            表示数:2, ループ:なし, 表示方向:縦, オートプレイ:なし
          </span>
          <Carousel7 />
        </div>
      </div>
    </div>
  );
};

export default CarouselPage;
src/components/carousel.tsx
"use client";

import { Card, CardContent } from "@/components/ui/card";
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel";
import { FC } from "react";
import Autoplay from "embla-carousel-autoplay";

export const Carousel1: FC = () => (
  <Carousel className="w-full max-w-xs">
    <CarouselContent>
      {Array.from({ length: 10 }).map((_, index) => (
        <CarouselItem key={index}>
          <div className="p-1">
            <Card>
              <CardContent className="flex flex-row aspect-square items-center justify-center p-6">
                <span className="text-4xl font-semibold">{index + 1}</span>
              </CardContent>
            </Card>
          </div>
        </CarouselItem>
      ))}
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
);

export const Carousel2: FC = () => (
  <Carousel
    opts={{
      align: "start",
    }}
    className="w-full max-w-sm"
  >
    <CarouselContent>
      {Array.from({ length: 10 }).map((_, index) => (
        // basisを調整することで表示する数を変更できる。basis-1/2の場合は2つ表示
        <CarouselItem key={index} className="basis-1/2">
          <div className="p-1">
            <Card>
              <CardContent className="flex aspect-square items-center justify-center p-6">
                <span className="text-3xl font-semibold">{index + 1}</span>
              </CardContent>
            </Card>
          </div>
        </CarouselItem>
      ))}
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
);

export const Carousel3: FC = () => (
  <Carousel
    opts={{
      align: "start",
    }}
    className="w-full max-w-sm"
  >
    <CarouselContent>
      {Array.from({ length: 10 }).map((_, index) => (
        // basisを調整することで表示する数を変更できる。basis-1/3の場合は3つ表示
        <CarouselItem key={index} className="basis-1/3">
          <div className="p-1">
            <Card>
              <CardContent className="flex aspect-square items-center justify-center p-6">
                <span className="text-3xl font-semibold">{index + 1}</span>
              </CardContent>
            </Card>
          </div>
        </CarouselItem>
      ))}
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
);

export const Carousel4: FC = () => (
  <Carousel
    opts={{
      align: "start",
      // loopで最後のスライドまで行ったら最初のスライドに戻るようになる。
      loop: true,
    }}
    className="w-full max-w-sm"
  >
    <CarouselContent>
      {Array.from({ length: 10 }).map((_, index) => (
        // basisを調整することで表示する数を変更できる。basis-1/3の場合は3つ表示
        <CarouselItem key={index} className="basis-1/3">
          <div className="p-1">
            <Card>
              <CardContent className="flex aspect-square items-center justify-center p-6">
                <span className="text-3xl font-semibold">{index + 1}</span>
              </CardContent>
            </Card>
          </div>
        </CarouselItem>
      ))}
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
);

export const Carousel5: FC = () => (
  <Carousel
    opts={{
      align: "start",
      // loopで最後のスライドまで行ったら最初のスライドに戻るようになる。
      loop: true,
    }}
    plugins={[
      // Autoplayで自動的にスライドを切り替えることができる。
      Autoplay({
        delay: 2000,
      }),
    ]}
    className="w-full max-w-sm"
  >
    <CarouselContent>
      {Array.from({ length: 10 }).map((_, index) => (
        // basisを調整することで表示する数を変更できる。
        // basis-1/2の場合は2つ表示、basis-1/3の場合は3つ表示
        <CarouselItem key={index} className="basis-1/3">
          <div className="p-1">
            <Card>
              <CardContent className="flex aspect-square items-center justify-center p-6">
                <span className="text-3xl font-semibold">{index + 1}</span>
              </CardContent>
            </Card>
          </div>
        </CarouselItem>
      ))}
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
);

export const Carousel6: FC = () => (
  <Carousel
    opts={{
      align: "start",
    }}
    className="w-full max-w-sm"
  >
    <CarouselContent>
      {Array.from({ length: 10 }).map((_, index) => (
        // basisを調整することで表示する数を変更できる。
        // デフォルトbasis-1/2の場合は2つ表示、mdの場合はbasis-1/4の場合は4つ表示
        <CarouselItem key={index} className="basis-1/2 md:basis-1/4">
          <div className="p-1">
            <Card>
              <CardContent className="flex aspect-square items-center justify-center p-6">
                <span className="text-3xl font-semibold">{index + 1}</span>
              </CardContent>
            </Card>
          </div>
        </CarouselItem>
      ))}
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
);

export const Carousel7: FC = () => (
  <Carousel
    opts={{
      align: "start",
    }}
    orientation="vertical"
    className="w-full max-w-xs"
  >
    <CarouselContent className="-mt-1 h-[200px]">
      {Array.from({ length: 5 }).map((_, index) => (
        <CarouselItem key={index} className="pt-1 basis-1/2">
          <div className="p-1">
            <Card>
              <CardContent className="flex items-center justify-center p-6">
                <span className="text-3xl font-semibold">{index + 1}</span>
              </CardContent>
            </Card>
          </div>
        </CarouselItem>
      ))}
    </CarouselContent>
    <CarouselPrevious />
    <CarouselNext />
  </Carousel>
);

解説

重要なポイントを解説します。

表示する個数を変更する方法は basis です。basis-1/2 で表示する個数は 2 個、basis-1/3 で表示する個数は 3 個となります。以下ではレスポンシブに表示する個数を変動させています。

<CarouselItem key={index} className="basis-1/2 md:basis-1/4">

loop:true により最後のスライドまで行ったら最初のスライドに戻ります。

<Carousel
  opts={{
    align: "start",
    // loopで最後のスライドまで行ったら最初のスライドに戻るようになる。
    loop: true,
  }}
  className="w-full max-w-sm"
>

Autoplay で自動的にスライドを切り替えることができます。

<Carousel
  opts={{
    align: "start",
  }}
  plugins={[
    // Autoplayで自動的にスライドを切り替えることができる。
    Autoplay({
      delay: 2000,
    }),
  ]}
  className="w-full max-w-sm"
>

レスポンシブに表示するアイテムの個数も変更できます。以下の記述では、デフォルトは表示件数 2 個、md からは表示件数 4 個となります。

<CarouselItem key={index} className="basis-1/2 md:basis-1/4">

動作確認

動作確認を行います。

$ pnpm dev

Alt text

作業結果を保存しておきます。

$ pnpm build
$ git add .
$ git commit -m "carouselを設定"

さいごに

本記事では、shadcn/ui に新しくリリースされた Carousel を触ってみました。

作業リポジトリは以下にあります。

https://github.com/hayato94087/next-shadcnui-sample

Discussion