پرش به محتوا

کاروسل

اسلایدشو قابل کشیدن برای گالری‌های محصول، نظرات مشتریان و بخش‌های قهرمان — با حاشیه‌های نئوبروتالیست و سایه‌های تند.

import * as React from "react"

import { Card, CardContent } from "@/components/ui/card"

کاروسل نواری از اسلایدها را — افقی یا عمودی — با ورودی drag و swipe، دکمه‌های قبلی/بعدی و یک API امری جابه‌جا می‌کند. بر پایهٔ کتابخانهٔ Embla Carousel ساخته شده و با دستور پخت نئوبروتالیستی استایل شده: حاشیه‌های ضخیم، سایه‌های سخت و تایپوگرافی پررنگ.

برای این موارد از آن استفاده کنید:

  • گالری محصول — اسکرین‌شات یا عکس‌ها را در یک اسلات ثابت ورق بزنید به‌جای اسکرول بلند.
  • نظر و دیوار لوگو — اثبات اجتماعی را بچرخانید، با autoplay از طریق پلاگین Embla.
  • آنبوردینگ و تور ویژگی — یک توالی کوتاه را کارت‌به‌کارت پیش ببرید.

درباره

کامپوننت carousel با کتابخانهٔ Embla Carousel ساخته شده است.

نصب

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/carousel.json
npx shadcn@latest add https://neobrutalism.com/r/base/carousel.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/carousel.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/carousel.json

وابستگی‌های زیر را نصب کنید:

pnpm add embla-carousel-react
npm install embla-carousel-react
yarn add embla-carousel-react
bun add embla-carousel-react

کد زیر را کپی کنید و در پروژهٔ خود بچسبانید.

components/ui/carousel.tsx
"use client"

import * as React from "react"
import useEmblaCarousel, {
  type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"

type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]

type CarouselProps = {
  opts?: CarouselOptions
  plugins?: CarouselPlugin
  orientation?: "horizontal" | "vertical"
  setApi?: (api: CarouselApi) => void
}

type CarouselContextProps = {
  carouselRef: ReturnType<typeof useEmblaCarousel>[0]
  api: ReturnType<typeof useEmblaCarousel>[1]
  scrollPrev: () => void
  scrollNext: () => void
  canScrollPrev: boolean
  canScrollNext: boolean
} & CarouselProps

const CarouselContext = React.createContext<CarouselContextProps | null>(null)

function useCarousel() {
  const context = React.useContext(CarouselContext)

  if (!context) {
    throw new Error("useCarousel must be used within a <Carousel />")
  }

  return context
}

function Carousel({
  orientation = "horizontal",
  opts,
  setApi,
  plugins,
  className,
  children,
  ...props
}: React.ComponentProps<"div"> & CarouselProps) {
  const [carouselRef, api] = useEmblaCarousel(
    {
      ...opts,
      axis: orientation === "horizontal" ? "x" : "y",
    },
    plugins
  )
  const [canScrollPrev, setCanScrollPrev] = React.useState(false)
  const [canScrollNext, setCanScrollNext] = React.useState(false)

  const onSelect = React.useCallback((api: CarouselApi) => {
    if (!api) return
    setCanScrollPrev(api.canScrollPrev())
    setCanScrollNext(api.canScrollNext())
  }, [])

  const scrollPrev = React.useCallback(() => {
    api?.scrollPrev()
  }, [api])

  const scrollNext = React.useCallback(() => {
    api?.scrollNext()
  }, [api])

  const handleKeyDown = React.useCallback(
    (event: React.KeyboardEvent<HTMLDivElement>) => {
      if (event.key === "ArrowLeft") {
        event.preventDefault()
        scrollPrev()
      } else if (event.key === "ArrowRight") {
        event.preventDefault()
        scrollNext()
      }
    },
    [scrollPrev, scrollNext]
  )

  React.useEffect(() => {
    if (!api || !setApi) return
    setApi(api)
  }, [api, setApi])

  React.useEffect(() => {
    if (!api) return
    onSelect(api)
    api.on("reInit", onSelect)
    api.on("select", onSelect)

    return () => {
      api?.off("reInit", onSelect)
      api?.off("select", onSelect)
    }
  }, [api, onSelect])

  return (
    <CarouselContext.Provider
      value={{
        carouselRef,
        api: api,
        opts,
        orientation:
          orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
        scrollPrev,
        scrollNext,
        canScrollPrev,
        canScrollNext,
      }}
    >
      <div
        onKeyDownCapture={handleKeyDown}
        className={cn("relative", className)}
        role="region"
        aria-roledescription="carousel"
        data-slot="carousel"
        {...props}
      >
        {children}
      </div>
    </CarouselContext.Provider>
  )
}

function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
  const { carouselRef, orientation } = useCarousel()

  return (
    <div
      ref={carouselRef}
      className="overflow-hidden"
      data-slot="carousel-content"
    >
      <div
        className={cn(
          "flex",
          orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
          className
        )}
        {...props}
      />
    </div>
  )
}

function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
  const { orientation } = useCarousel()

  return (
    <div
      role="group"
      aria-roledescription="slide"
      data-slot="carousel-item"
      className={cn(
        "min-w-0 shrink-0 grow-0 basis-full",
        orientation === "horizontal" ? "pl-4" : "pt-4",
        className
      )}
      {...props}
    />
  )
}

function CarouselPrevious({
  className,
  variant = "outline",
  size = "icon-sm",
  ...props
}: React.ComponentProps<typeof Button>) {
  const { orientation, scrollPrev, canScrollPrev } = useCarousel()

  return (
    <Button
      data-slot="carousel-previous"
      variant={variant}
      size={size}
      className={cn(
        "absolute size-8 touch-manipulation rounded",
        orientation === "horizontal"
          ? "top-1/2 -left-12 -translate-y-1/2 hover:-translate-y-[calc(50%-2px)] active:-translate-y-[calc(50%-4px)]"
          : "-top-12 left-1/2 -translate-x-1/2 rotate-90 hover:-translate-x-[calc(50%-2px)] active:-translate-x-[calc(50%-4px)]",
        className
      )}
      disabled={!canScrollPrev}
      onClick={scrollPrev}
      {...props}
    >
      <ArrowLeftIcon className="cn-rtl-flip" />
      <span className="sr-only">Previous slide</span>
    </Button>
  )
}

function CarouselNext({
  className,
  variant = "outline",
  size = "icon-sm",
  ...props
}: React.ComponentProps<typeof Button>) {
  const { orientation, scrollNext, canScrollNext } = useCarousel()

  return (
    <Button
      data-slot="carousel-next"
      variant={variant}
      size={size}
      className={cn(
        "absolute size-8 touch-manipulation rounded",
        orientation === "horizontal"
          ? "top-1/2 -right-12 -translate-y-1/2 hover:-translate-y-[calc(50%-2px)] active:-translate-y-[calc(50%-4px)]"
          : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90 hover:-translate-x-[calc(50%-2px)] active:-translate-x-[calc(50%-4px)]",
        className
      )}
      disabled={!canScrollNext}
      onClick={scrollNext}
      {...props}
    >
      <ArrowRightIcon className="cn-rtl-flip" />
      <span className="sr-only">Next slide</span>
    </Button>
  )
}

export {
  type CarouselApi,
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
  useCarousel,
}

مسیرهای import را متناسب با ساختار پروژهٔ خود به‌روزرسانی کنید.

استفاده

import {
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/components/ui/carousel"
<Carousel>
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

ترکیب

برای ساخت Carousel از ترکیب زیر استفاده کنید:

Carousel
├── CarouselContent
│   ├── CarouselItem
│   └── CarouselItem
├── CarouselPrevious
└── CarouselNext

نمونه‌ها

اندازه‌ها

برای تنظیم اندازهٔ آیتم‌ها، می‌توانید از کلاس یوتیلیتی basis روی <CarouselItem /> استفاده کنید.

import * as React from "react"

import { Card, CardContent } from "@/components/ui/card"
// 33% of the carousel width.
<Carousel>
  <CarouselContent>
    <CarouselItem className="basis-1/3">...</CarouselItem>
    <CarouselItem className="basis-1/3">...</CarouselItem>
    <CarouselItem className="basis-1/3">...</CarouselItem>
  </CarouselContent>
</Carousel>
// 50% on small screens and 33% on larger screens.
<Carousel>
  <CarouselContent>
    <CarouselItem className="md:basis-1/2 lg:basis-1/3">...</CarouselItem>
    <CarouselItem className="md:basis-1/2 lg:basis-1/3">...</CarouselItem>
    <CarouselItem className="md:basis-1/2 lg:basis-1/3">...</CarouselItem>
  </CarouselContent>
</Carousel>

فاصله‌گذاری

برای تنظیم فاصلهٔ بین آیتم‌ها، از یک یوتیلیتی pl-[VALUE] روی <CarouselItem /> و یک -ml-[VALUE] منفی روی <CarouselContent /> استفاده می‌کنیم.

import * as React from "react"

import { Card, CardContent } from "@/components/ui/card"
<Carousel>
  <CarouselContent className="-ml-4">
    <CarouselItem className="pl-4">...</CarouselItem>
    <CarouselItem className="pl-4">...</CarouselItem>
    <CarouselItem className="pl-4">...</CarouselItem>
  </CarouselContent>
</Carousel>
<Carousel>
  <CarouselContent className="-ml-2 md:-ml-4">
    <CarouselItem className="pl-2 md:pl-4">...</CarouselItem>
    <CarouselItem className="pl-2 md:pl-4">...</CarouselItem>
    <CarouselItem className="pl-2 md:pl-4">...</CarouselItem>
  </CarouselContent>
</Carousel>

جهت‌گیری

از پراپ orientation برای تنظیم جهت‌گیری carousel استفاده کنید.

import * as React from "react"

import { Card, CardContent } from "@/components/ui/card"
<Carousel orientation="vertical | horizontal">
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
</Carousel>

گزینه‌ها

می‌توانید با پراپ opts گزینه‌هایی را به carousel پاس دهید. برای اطلاعات بیشتر مستندات Embla Carousel را ببینید.

<Carousel
  opts={{
    align: "start",
    loop: true,
  }}
>
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
</Carousel>

API

از یک state و پراپ setApi برای گرفتن یک نمونه از API carousel استفاده کنید.

"use client"

import * as React from "react"
import { type CarouselApi } from "@/components/ui/carousel"
 
export function Example() {
  const [api, setApi] = React.useState<CarouselApi>()
  const [current, setCurrent] = React.useState(0)
  const [count, setCount] = React.useState(0)
 
  React.useEffect(() => {
    if (!api) {
      return
    }
 
    setCount(api.scrollSnapList().length)
    setCurrent(api.selectedScrollSnap() + 1)
 
    api.on("select", () => {
      setCurrent(api.selectedScrollSnap() + 1)
    })
  }, [api])
 
  return (
    <Carousel setApi={setApi}>
      <CarouselContent>
        <CarouselItem>...</CarouselItem>
        <CarouselItem>...</CarouselItem>
        <CarouselItem>...</CarouselItem>
      </CarouselContent>
    </Carousel>
  )
}

رویدادها

می‌توانید با نمونهٔ api از setApi به رویدادها گوش دهید.

import { type CarouselApi } from "@/components/ui/carousel"
 
export function Example() {
  const [api, setApi] = React.useState<CarouselApi>()
 
  React.useEffect(() => {
    if (!api) {
      return
    }
 
    api.on("select", () => {
      // Do something on select.
    })
  }, [api])
 
  return (
    <Carousel setApi={setApi}>
      <CarouselContent>
        <CarouselItem>...</CarouselItem>
        <CarouselItem>...</CarouselItem>
        <CarouselItem>...</CarouselItem>
      </CarouselContent>
    </Carousel>
  )
}

برای اطلاعات بیشتر دربارهٔ استفاده از رویدادها، مستندات Embla Carousel را ببینید.

Plugins

می‌توانید از پراپ plugins برای افزودن پلاگین‌ها به carousel استفاده کنید.

import Autoplay from "embla-carousel-autoplay"
 
export function Example() {
  return (
    <Carousel
      plugins={[
        Autoplay({
          delay: 2000,
        }),
      ]}
    >
      // ...
    </Carousel>
  )
}
"use client"

import * as React from "react"

RTL

برای فعال‌سازی پشتیبانی RTL در Neobrutalism، راهنمای پیکربندی RTL را ببینید.

"use client"

import * as React from "react"

هنگام بومی‌سازی carousel برای زبان‌های RTL، باید گزینهٔ direction را در پراپ opts طوری تنظیم کنید که با جهت متن مطابقت داشته باشد. این تضمین می‌کند که carousel در جهت درست اسکرول می‌شود.

<Carousel
  dir={dir}
  opts={{
    direction: dir,
  }}
>
  <CarouselContent>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
    <CarouselItem>...</CarouselItem>
  </CarouselContent>
  <CarouselPrevious className="rtl:rotate-180" />
  <CarouselNext className="rtl:rotate-180" />
</Carousel>

گزینهٔ direction مقدار "ltr" یا "rtl" را می‌پذیرد و باید با مقدار پراپ dir مطابقت داشته باشد. ممکن است بخواهید دکمه‌های ناوبری را نیز با کلاس rtl:rotate-180 بچرخانید تا در جهت درست اشاره کنند.

دسترسی‌پذیری

کاروسل از الگوی WAI-ARIA Carousel پیروی می‌کند: ریشه به‌صورت role="region" با aria-roledescription="carousel" رندر می‌شود، هر اسلاید role="group" با aria-roledescription="slide" است، و وقتی کاروسل loop نمی‌کند دکمه‌های قبلی/بعدی در انتها disable می‌شوند. Embla ورودی اشاره‌گر و لمس را مدیریت می‌کند؛ wrapper لایهٔ صفحه‌کلید را اضافه می‌کند.

تعاملات صفحه‌کلید:

کلیدعمل
Tab / Shift + Tabانتقال فوکوس به دکمهٔ ناوبری قبلی / بعدی
Space / Enterفعال‌سازی دکمهٔ ناوبری فوکوس‌شده
ArrowLeft / ArrowRightاسکرول به اسلاید قبلی / بعدی وقتی فوکوس داخل کاروسل است

مرجع API

برای اطلاعات بیشتر دربارهٔ پراپ‌ها و پلاگین‌ها، مستندات Embla Carousel را ببینید.