کاروسل
اسلایدشو قابل کشیدن برای گالریهای محصول، نظرات مشتریان و بخشهای قهرمان — با حاشیههای نئوبروتالیست و سایههای تند.
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.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/carousel.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/carousel.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/carousel.json
وابستگیهای زیر را نصب کنید:
pnpm add embla-carousel-reactnpm install embla-carousel-reactyarn add embla-carousel-reactbun add embla-carousel-react
کد زیر را کپی کنید و در پروژهٔ خود بچسبانید.
"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 را ببینید.