عارض الشرائح (Carousel)

عارض شرائح مبني على scroll-snap مع مؤشرات الشرائح: نقطة لكل شريحة، وتستطيل النقطة النشطة إلى حبة ممدودة.

التثبيت

bash
npx @smicolon/fasla-ui@latest add carousel

قبل إضافة أي مكوّن، شغّل الأمر init مرة واحدة في مشروعك. التفاصيل في صفحة التثبيت.

معاينة

المسار شريط تمرير أصلي مثبّت بخاصية scroll-snap، فيتحرك بالسحب ولوحة التتبّع والنقاط على حد سواء، وتتبع النقطة النشطة تمريرك في أي اتجاه.

البلدة القديمة عند الفجر
سوق الميناء
الكورنيش عند الغروب

الأنماط (variant)

محور No. of items في Figma هو ببساطة عدد الشرائح التي تعرضها: صف النقاط يتبع عدد الشرائح، والحبة الممدودة هي Active Item.

2
الشريحة 1
الشريحة 2
3
الشريحة 1
الشريحة 2
الشريحة 3
4
الشريحة 1
الشريحة 2
الشريحة 3
الشريحة 4

الحالات

النقطة النشطة حبة ممدودة بعرض 24px بلون primary، والبقية دوائر 8px بلون muted-foreground، ويتحرك الامتداد بينهما ما لم تكن الحركة مخفّضة. تبدأ defaultActiveIndex من شريحة لاحقة بقفزة فورية دون تمرير.

البلدة القديمة عند الفجر
سوق الميناء
الكورنيش عند الغروب

مؤشرات الشرائح وحدها

إذا أعطيت CarouselDots قيمة count فلن يحتاج إلى عارض شرائح حوله: يعرض activeIndex ويُبلغ عن كل ضغطة عبر onActiveIndexChange، فيمكن لمعرض صور أو لخطوات متتابعة أن يتحكم فيه بحالته الخاصة.

البلدة القديمة عند الفجر

إتاحة الوصول (accessibility)

  • الجذر منطقة region مع aria-roledescription بقيمة carousel. مرّر دائمًا aria-label لتسميته.
  • كل شريحة مجموعة group تُسمّى بالأرقام "2 / 3"، فلا يحتاج الاسم إلى ترجمة؛ مرّر aria-label على الشريحة لتسميتها بمحتواها.
  • مؤشرات الشرائح أزرار حقيقية: يصل إليها مفتاح Tab، وتحمل النقطة النشطة aria-current، وتعيد label تسميتها بلغتك.
  • المسار محطة تركيز، ومفاتيح الأسهم تنقل شريحة واحدة من أي موضع في المكوّن وفق اتجاه النص، فالمفتاح الذي يشير إلى الأمام على الشاشة يتقدم دائمًا. وينتقل بمفتاح Home إلى أول شريحة وبمفتاح End إلى آخرها.
  • يرسم التركيز من لوحة المفاتيح حلقة بعرض 2px، على بعد 2px من النقطة أو من المسار، باللون ring. وتتبع الحلقة على النقطة شكل الدائرة أو الحبة الممدودة، لا المساحة الأكبر التي تقبل الضغط.
  • التمرير السلس يأتي من CSS وحدها، لذا تجعل prefers-reduced-motion كل حركة فورية دون أي JavaScript.
  • الاتجاه يُورَث من السمة dir وليس خاصية. المسار والتثبيت والنقاط كلها تتبع اتجاه السطر.

الخصائص (props)

Carousel

الخاصيةالنوعالافتراضيالوصف
activeIndexnumberلا يوجدالشريحة النشطة عند التحكم بالحالة من الخارج.
defaultActiveIndexnumber0الشريحة المعروضة في البداية.
onActiveIndexChange(index) => voidلا يوجدتُستدعى عند تغيّر الشريحة النشطة، سواء من النقاط أو من التمرير اليدوي.
aria-labelstringلا يوجديسمّي المكوّن للتقنيات المساعدة. مرّره دائمًا، أو مرّر aria-labelledby بدلًا منه.
childrenReactNodeلا يوجدالمسار CarouselContent ومؤشرات الشرائح CarouselDots.
classNamestringلا يوجدأصناف إضافية للجزء.

CarouselContent

الخاصيةالنوعالافتراضيالوصف
childrenReactNodeلا يوجدالشرائح، لكل شريحة CarouselItem.
classNamestringلا يوجدأصناف إضافية للجزء.

CarouselItem

الخاصيةالنوعالافتراضيالوصف
aria-labelstringلا يوجديسمّي الشريحة بمحتواها. والاسم الافتراضي موضعها، مثل "2 / 3".
childrenReactNodeلا يوجدمحتوى الشريحة.
classNamestringلا يوجدأصناف إضافية. بدّل basis-full لعرض أكثر من شريحة في المرة الواحدة.

CarouselDots

الخاصيةالنوعالافتراضيالوصف
countnumberلا يوجدعدد النقاط، عند استخدام المؤشرات وحدها. اتركه داخل عارض الشرائح، فهو يعدّ شرائحه بنفسه.
activeIndexnumber0النقطة النشطة، عند استخدام المؤشرات وحدها.
onActiveIndexChange(index) => voidلا يوجدتُستدعى برقم النقطة عند الضغط عليها، عند استخدام المؤشرات وحدها.
label(index, count) => stringلا يوجدتسمّي كل نقطة بلغتك. والاسم الافتراضي موضعها، مثل "2 / 3".
classNamestringلا يوجدأصناف إضافية للجزء.

الاستخدام

شرائح مع مؤشراتها

tsx
import {
  Carousel,
  CarouselContent,
  CarouselDots,
  CarouselItem,
} from "@/components/ui/carousel"

export function CityGuide() {
  return (
    <Carousel aria-label="دليل المدينة" className="space-y-4">
      <CarouselContent>
        <CarouselItem>البلدة القديمة عند الفجر</CarouselItem>
        <CarouselItem>سوق الميناء</CarouselItem>
        <CarouselItem>الكورنيش عند الغروب</CarouselItem>
      </CarouselContent>
      <CarouselDots />
    </Carousel>
  )
}

البدء من شريحة لاحقة

tsx
import {
  Carousel,
  CarouselContent,
  CarouselDots,
  CarouselItem,
} from "@/components/ui/carousel"

export function CityGuide() {
  return (
    <Carousel aria-label="دليل المدينة" defaultActiveIndex={2} className="space-y-4">
      <CarouselContent>
        <CarouselItem>البلدة القديمة عند الفجر</CarouselItem>
        <CarouselItem>سوق الميناء</CarouselItem>
        <CarouselItem>الكورنيش عند الغروب</CarouselItem>
      </CarouselContent>
      <CarouselDots />
    </Carousel>
  )
}

مؤشرات الشرائح وحدها

tsx
"use client"

import * as React from "react"
import { CarouselDots } from "@/components/ui/carousel"

const PHOTOS = ["البلدة القديمة عند الفجر", "سوق الميناء", "الكورنيش عند الغروب"]

export function PhotoStepper() {
  const [index, setIndex] = React.useState(0)
  return (
    <div className="flex flex-col gap-4">
      <p className="text-sm font-medium">{PHOTOS[index]}</p>
      <CarouselDots count={PHOTOS.length} activeIndex={index} onActiveIndexChange={setIndex} />
    </div>
  )
}

التحكم بالحالة

tsx
"use client"

import * as React from "react"
import {
  Carousel,
  CarouselContent,
  CarouselDots,
  CarouselItem,
} from "@/components/ui/carousel"

export function CityGuide() {
  const [index, setIndex] = React.useState(0)
  return (
    <Carousel
      aria-label="دليل المدينة"
      activeIndex={index}
      onActiveIndexChange={setIndex}
      className="space-y-4"
    >
      <CarouselContent>
        <CarouselItem>البلدة القديمة عند الفجر</CarouselItem>
        <CarouselItem>سوق الميناء</CarouselItem>
        <CarouselItem>الكورنيش عند الغروب</CarouselItem>
      </CarouselContent>
      <CarouselDots />
    </Carousel>
  )
}