IlmHamroh
JavaScript Full-stack/20-qism. React: UI kutubxonalar va animatsiya13/30-dars23 daqiqa
Mundarija (24)

Karusel va slayderlar React'da: o'z karuselingiz, Embla va Swiper, avto-aylanish va a11y

Qisqacha: Karusel kerakmi — avval shuni so'rang: ko'pincha CSS scroll-snap lentasi yetadi va u 0 KB JavaScript. React'da o'z karuselingiz — bitta index state, «Oldingi»/«Keyingi» tugmalari, strelkalar va to'g'ri ARIA (aria-roledescription, role="group"). Avto-aylanish faqat to'xtatish tugmasi bilan, sichqoncha ustida va fokusda pauza, prefers-reduced-motion da — o'chiq. Sensorli surish, cheksiz aylanish va sinxron karusellar uchun kutubxona: Embla (shadcn Carousel asosi, o'lchovimizda +7.7 KB gzip, CSP buzilishi 0) yoki Swiper (modullar, breakpoints, thumbs).

Bu darsda

  • React'da o'z karuselingizni yozasiz: state, tugmalar, klaviatura va ekran o'quvchi uchun e'lon.
  • Avto-aylanish hookini qurasiz: to'xtatish tugmasi, hover va fokusda pauza, harakatni kamaytirish sozlamasi.
  • Embla'ning React API'sini (useEmblaCarousel, hodisalar, Autoplay plagini) va shadcn Carousel'ni o'qiysiz.
  • Swiper'ning modullari, breakpoints va thumbs bilan tanishasiz.
  • Karuseldagi rasmlarni to'g'ri yuklaysiz: birinchisi tez, qolgani dangasa.
  • Kutubxona narxini o'lchov bilan baholaysiz va vazifalar da nega karusel yo'qligini tushuntirasiz.

Oldin bilishingiz kerak: CSS karusel va gorizontal skroll lentasi, useRef II: DOM elementlari, useEffect III: cleanup, Yuklanish, bo'sh va xato holatlari UX.

1. Nega bu kerak?

CSS karusel darsida JavaScript'siz lenta qurganmiz: scroll-snap, ::scroll-button va ::scroll-marker. U yerda qoidalar ham aytilgan edi: avto-aylanish deyarli har doim yomon, to'xtatish tugmasi majburiy, slayd — role="group".

Endi «Bahor» saytining bosh sahifasi. Jasur aka shunday so'radi:

  1. "Kun taomlari" — uchta katta rasm, bittadan ko'rinsin, o'zi almashsin.
  2. Telefonda barmoq bilan surilsin, kompyuterda sichqoncha bilan "tortib" ham.
  3. Oxirgisidan keyin yana birinchisi kelsin (cheksiz).

Birinchi va uchinchi talabni CSS bajara olmaydi: vaqt bo'yicha surish va "oxiridan boshiga" — JavaScript ishi. Kompyuterda sichqoncha bilan tortish ham CSS'da yo'q. Shu joyda React va kutubxonalar kerak bo'ladi.

Lekin bu darsning eng muhim savoli boshqacha: karusel kerakmi? Bosh sahifadagi uchta slayddan ikkinchisi va uchinchisini ko'pchilik hech qachon ko'rmaydi. Hayotiy o'xshatish: do'kon vitrinasi. Mollar aylanib tursa, odam to'xtab qaramaydi — u faqat birinchisini ko'radi. Uchala taomni yonma-yon qo'yish ko'pincha foydaliroq.

Karusel o'rniga nima qo'yish mumkin:

Vaziyat Karusel o'rniga
3–4 ta teng muhim narsa to'r (grid): hammasi bir paytda ko'rinadi
ko'p kartochka, telefonda gorizontal lenta (scroll-snap), keyingisi chetdan ko'rinib tursin
bir xil joyda turli bo'limlar tablar (Tabs va accordion)
bitta asosiy xabar bitta katta banner, karuselsiz

Karusel haqiqatan o'rinli joylar ham bor: mahsulot rasmlari galereyasi (bir mahsulot, ko'p rasm), qadamma-qadam tanishtiruv (onboarding) va fotoalbom. Ularda foydalanuvchi o'zi ketma-ket ko'rishni xohlaydi.

2. Karusel anatomiyasi

Har karusel — kutubxonali ham, kutubxonasiz ham — bir xil qismlardan iborat:

flowchart TB
  R["section — karusel<br/>aria-label"] --> P["To'xtatish tugmasi<br/>(birinchi)"]
  R --> V["Ko'rish oynasi (viewport)<br/>overflow: hidden"]
  V --> C["Konteyner — slaydlar qatori<br/>display: flex"]
  C --> S["Slayd — role=group<br/>aria-label: 3 dan 1"]
  R --> B["Oldingi / Keyingi"]
  R --> D["Nuqtalar (ixtiyoriy)"]

Nimaga qarang: ko'rish oynasi — "derazacha", konteyner esa uning orqasida chapga-o'ngga suriladi. Kutubxonalar aynan shu qatorni surish bilan shug'ullanadi: Embla va Swiper konteynerni transform bilan siljitadi, CSS lentasi esa brauzerning o'z skrolini ishlatadi.

3. React'da o'z karuselingiz

3.1 Bitta state — index

Eng sodda karusel uchun kutubxona shart emas. Hamma narsa bitta sondan kelib chiqadi: hozir nechanchi slayd ko'rinib turibdi. «Ishga tushir» dan keyin tugmalarni va strelkalarni sinang (avval «Keyingi» ga bosib, fokusni karusel ichiga oling):

jsx
import { useState } from "react";

const DISHES = [
  "Osh — 35 000 so'm",
  "Lag'mon — 28 000 so'm",
  "Manti — 30 000 so'm",
];

export default function App() {
  const [index, setIndex] = useState(0);
  const count = DISHES.length;
  // Halqa: oxiridan keyin — boshi, boshidan oldin — oxiri
  const go = (i) => setIndex((i + count) % count);
  function onKeyDown(e) {
    if (e.key === "ArrowRight") go(index + 1);
    if (e.key === "ArrowLeft") go(index - 1);
  }
  return (
    <section
      aria-roledescription="karusel"
      aria-label="Kun taomlari"
      onKeyDown={onKeyDown}
    >
      <div aria-live="polite">
        {DISHES.map((dish, i) => (
          <div
            key={dish}
            role="group"
            aria-roledescription="slayd"
            aria-label={`${count} dan ${i + 1}`}
            hidden={i !== index}
          >
            {dish}
          </div>
        ))}
      </div>
      <button onClick={() => go(index - 1)}>Oldingi</button>{" "}
      <button onClick={() => go(index + 1)}>Keyingi</button>
    </section>
  );
}

Ekranda:

text
Manti — 30 000 so'm
Oldingi Keyingi

«Keyingi» bosildi — Lag'mon, keyin → — Manti. Yana bir → — yana Osh: (2 + 1) % 3 = 0. Bu halqa — qoldiqli bo'lish (%) bilan yasaladi.

Kodni qatorma-qator o'qiymiz:

  • (i + count) % count — -1 ni ham to'g'ri aylantiradi: (-1 + 3) % 3 = 2. Faqat i % count yozilsa, JavaScript'da -1 % 3 → -1 va slayd topilmaydi.
  • hidden={i !== index} — boshqa slaydlar DOM'da qoladi, lekin ko'rinmaydi va ekran o'quvchi ularni o'qimaydi. Rasmlar bo'lsa, ular oldindan yuklanib turadi — keyingi slayd darhol chiqadi.
  • onKeyDown — section da. Strelka fokus karusel ichidagi istalgan elementda turganda ishlaydi. Butun sahifaga (window) ulash xato: matn maydonida yozayotgan foydalanuvchining strelkalari slaydni almashtirib yuborardi.
  • aria-live="polite" — Malika «Keyingi» ni bosganda yangi slayd matnini eshitadi. Avto-aylanishda esa buni o'chiramiz (keyingi bo'lim).

aria-roledescription qiymati — o'zbekcha: ekran o'quvchi uni AYNAN o'qiydi. Interfeys o'zbekcha bo'lsa, "carousel" emas, "karusel" (CSS karusel darsidagidek).

Tekshirib ko'ring: Nega key={dish} va key={i} emas? Bu misolda farqi bormi?

Javob

Bu misolda ro'yxat o'zgarmaydi, shuning uchun farq ko'rinmaydi. Lekin karusel ma'lumoti serverdan kelsa va tartibi o'zgarsa (yangi taom boshiga qo'shildi), key={i} bilan React slaydlarni noto'g'ri "qayta ishlatadi": eski slaydning ichki holati (masalan, rasm yuklangani) yangisiga o'tib qoladi. Barqaror kalit — taom nomi yoki id (key darsidagi qoida).

3.2 Avto-aylanish: qoidalar kodda

CSS karusel darsidagi to'rt qoidani endi kodga aylantiramiz:

  1. To'xtatish tugmasi — karuseldan oldin, Tab bilan birinchi.
  2. Sichqoncha ustida yoki fokus ichida — pauza.
  3. prefers-reduced-motion: reduce — umuman yoqilmaydi.
  4. Aylanayotganda jonli hudud o'chiq (aria-live="off"), aks holda Malika har 5 soniyada gapni bo'ladi.

Darsda oraliq 500 ms — natijani tez ko'rish uchun. Loyihada 5–7 soniya.

jsx
import { useEffect, useRef, useState } from "react";

const DISHES = ["Osh", "Lag'mon", "Manti"];

function prefersReducedMotion() {
  // matchMedia — 13-qism: CSS media query'ni JS'dan tekshirish
  const query = "(prefers-reduced-motion: reduce)";
  return window.matchMedia(query).matches;
}

// playing — foydalanuvchi to'xtatmagan; paused — hover yoki fokus
function useAutoplay(onTick, delay, playing, paused) {
  const tick = useRef(onTick);
  tick.current = onTick;
  useEffect(() => {
    if (!playing || paused) return;
    const id = setInterval(() => tick.current(), delay);
    return () => clearInterval(id);
  }, [delay, playing, paused]);
}

export default function App() {
  const [index, setIndex] = useState(0);
  const [playing, setPlaying] = useState(
    () => !prefersReducedMotion(),
  );
  const [hover, setHover] = useState(false);
  const [focus, setFocus] = useState(false);
  const count = DISHES.length;
  useAutoplay(
    () => setIndex((i) => (i + 1) % count),
    500,
    playing,
    hover || focus,
  );
  const first = useRef(true);
  useEffect(() => {
    if (first.current) {
      first.current = false;
      return;
    }
    console.log("slayd:", index + 1);
  }, [index]);
  return (
    <section
      aria-roledescription="karusel"
      aria-label="Kun taomlari"
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      onFocus={() => setFocus(true)}
      onBlur={() => setFocus(false)}
    >
      <button onClick={() => setPlaying(!playing)}>
        {playing ? "To'xtatish" : "Davom ettirish"}
      </button>
      <div aria-live={playing ? "off" : "polite"}>
        {DISHES.map((dish, i) => (
          <div
            key={dish}
            role="group"
            aria-roledescription="slayd"
            aria-label={`${count} dan ${i + 1}`}
            hidden={i !== index}
          >
            {dish}
          </div>
        ))}
      </div>
    </section>
  );
}

Konsolda:

text
slayd: 2

Ekranda:

text
Davom ettirish
Lag'mon

500 ms da ikkinchi slayd keldi, 700 ms da «To'xtatish» bosildi va keyingi 1.2 soniyada hech narsa o'zgarmadi. Tugma matni endi «Davom ettirish».

Hookdagi uch detal:

  • tick.current = onTick — latest ref naqshi. Interval har renderda qayta yaratilmaydi, lekin doim eng yangi funksiyani chaqiradi. Aks holda onTick har render yangi bo'lgani uchun effekt har slaydda qayta ishga tushardi va taymer "nolga qaytardi".
  • Cleanup — clearInterval. Pauza, to'xtatish yoki komponent yo'qolishi — taymer o'chadi. Cleanup bo'lmasa, sahifadan chiqib ketganda ham interval ishlab, yo'q komponentning state'ini o'zgartirishga urinadi.
  • useState(() => !prefersReducedMotion()) — boshlang'ich qiymat funksiya: matchMedia faqat birinchi renderda bir marta chaqiriladi.

Diqqat: onFocus/onBlur React'da ichki elementlardan ham ko'tariladi (DOM'dagi focusin/focusout kabi). Shuning uchun fokus karusel ichidagi istalgan tugmada turganda pauza ishlaydi. Lekin fokus bir tugmadan qo'shni tugmaga o'tganda blur va focus ketma-ket keladi — oraliqda bir lahza focus = false. Taymer 5 soniyalik bo'lsa, bu sezilmaydi.

3.3 Gibrid: CSS lenta va React tugmalari

Ko'p hollarda eng yaxshi yo'l — ikkalasini qo'shish. Surish, snap va barmoq bilan surish — brauzerning o'z skroli (Skroll xulqi va scroll-snap). React esa faqat tugmalarni beradi: ref orqali lentani bir slayd eniga suradi.

Natija oynasi brauzerda — skrol bor. Tekshiruv muhitida (jsdom) esa layout va scrollBy yo'q, shuning uchun bu blok tekshirilmagan: «Ishga tushir» ni bosing va tugmalarni sinang.

jsx
import { useRef } from "react";

const DISHES = ["Osh", "Lag'mon", "Manti", "Somsa"];

const css = `
  .strip { display: flex; gap: 8px; overflow-x: auto;
    scroll-snap-type: x mandatory; overscroll-behavior-x: contain; }
  .strip > li { flex: 0 0 80%; scroll-snap-align: start;
    list-style: none; padding: 24px; background: #eef3ee; }
  @media (prefers-reduced-motion: no-preference) {
    .strip { scroll-behavior: smooth; }
  }
`;

export default function App() {
  const strip = useRef(null);
  function scroll(direction) {
    const el = strip.current;
    // bitta "slayd" eni — birinchi bolaning eni + oraliq
    const step = el.firstElementChild.offsetWidth + 8;
    el.scrollBy({ left: direction * step });
    setTimeout(() => console.log("scrollLeft:", el.scrollLeft), 600);
  }
  return (
    <section aria-label="Kun taomlari">
      <style>{css}</style>
      <ul className="strip" ref={strip} tabIndex={0}>
        {DISHES.map((d) => (
          <li key={d}>{d}</li>
        ))}
      </ul>
      <button onClick={() => scroll(-1)}>Oldingi</button>{" "}
      <button onClick={() => scroll(1)}>Keyingi</button>
    </section>
  );
}

Biz Chrome 155 da sinadik: eni 600 px oyna, «Keyingi» ikki marta bosildi. Konsolda:

text
scrollLeft: 531
scrollLeft: 1022

Raqamlar oyna eniga va ro'yxatning ichki chekinishiga (padding) bog'liq — sizda boshqacha chiqadi. Muhimi: har bosishda lenta bir slaydga surildi va snap uni slayd boshiga to'g'rilab qo'ydi. Uch nuqta:

  • scroll-behavior: smooth — CSS'da, faqat no-preference da. scrollBy ga behavior bermadik: brauzer CSS'ga qaraydi. Harakatni kamaytirish so'ralganda lenta silliq emas, darhol suriladi — JavaScript'da matchMedia tekshiruvi shart emas.
  • tabIndex={0} — lentaga Tab bilan kelib, strelkalar bilan surish mumkin. Skrollanadigan hudud klaviaturaga ochiq bo'lishi kerak (axe buni scrollable-region-focusable qoidasi bilan tekshiradi).
  • Snap o'zi to'g'rilaydi. step bir necha piksel noto'g'ri bo'lsa ham, brauzer lentani eng yaqin snap nuqtasiga "yopishtiradi".

Halqa (oxiridan boshiga) bu usulda yo'q — u kutubxona ishi.

3.4 Barmoq bilan surish: pointer hodisalari

index li karuselda 3.1-bob slaydlar skrollanmaydi — barmoq bilan surishni o'zimiz tutishimiz kerak. G'oya: barmoq qo'yilgan joyni eslab qolamiz, ko'tarilganda farqni (dx) hisoblaymiz. Farq 50 px dan katta bo'lsa — slayd almashadi. Pointer hodisalari sichqoncha, barmoq va qalamni bitta API bilan beradi.

jsx
import { useRef, useState } from "react";

const DISHES = ["Osh", "Lag'mon", "Manti"];
const THRESHOLD = 50; // px — tasodifiy tegishdan ajratish uchun

export default function App() {
  const [index, setIndex] = useState(0);
  const startX = useRef(null);
  const count = DISHES.length;
  function onPointerDown(e) {
    startX.current = e.clientX;
    e.currentTarget.setPointerCapture(e.pointerId);
  }
  function onPointerUp(e) {
    if (startX.current === null) return;
    const dx = e.clientX - startX.current;
    startX.current = null;
    console.log("dx:", Math.round(dx));
    if (Math.abs(dx) < THRESHOLD) return;
    setIndex((i) => (i + (dx < 0 ? 1 : -1) + count) % count);
  }
  return (
    <div
      onPointerDown={onPointerDown}
      onPointerUp={onPointerUp}
      style={{
        touchAction: "pan-y",
        userSelect: "none",
        padding: 32,
        background: "#eef3ee",
      }}
    >
      {DISHES[index]}
    </div>
  );
}

Chrome 155 da sichqoncha bilan chapga 120 px tortildi (puppeteer, haqiqiy sichqoncha hodisalari):

text
dx: -120

Ekranda «Lag'mon» qoldi. Kodda uch muhim narsa:

  • touchAction: "pan-y" — brauzerga aytamiz: "tik yo'nalishdagi surish — sahifa skroli, gorizontali — meniki". Busiz telefon gorizontal harakatni o'zi ushlab, pointercancel yuboradi va pointerup kelmaydi.
  • setPointerCapture — barmoq elementdan chiqib ketsa ham pointerup shu elementga keladi.
  • Chegara (50 px) — tugmani bosmoqchi bo'lgan odamning barmog'i bir oz siljisa, slayd almashmasin.

Bu "eng sodda" surish: slayd barmoqqa ergashmaydi, tezlik (inertsiya) va chekkadagi "rezina" yo'q. Aynan shu narsalar — tabiiy his, tezlikka qarab bir nechta slayd o'tkazish, 60 kadr/s — kutubxonalarning asosiy ishi. Shu joyda Embla va Swiper o'zini oqlaydi.

4.1 Nega Embla

Embla Carousel — React uchun eng ko'p tavsiya qilinadigan yengil karusel. shadcn/ui'ning Carousel komponenti aynan Embla ustiga qurilgan. Embla headlessga yaqin: u faqat harakatni (surish, tortish, snap, halqa) beradi. Tugmalar, nuqtalar, rasmlar va CSS — sizniki (UI kutubxonalar xaritasi darsidagi atamalar bilan).

Versiya: embla-carousel-react 8.6.0 (2025-yil aprel, npm'dagi eng yangisi).

Natija oynasida Embla yo'q, shuning uchun bu bo'limdagi kod — o'z loyihangiz uchun:

bash
npm i -E embla-carousel-react@8.6.0 embla-carousel-autoplay@8.6.0

4.2 Asosiy misol

Embla ikki narsa qaytaradi: DOM uchun ref va boshqaruv uchun API obyekti:

jsx
import { useCallback, useEffect, useState } from "react";
import useEmblaCarousel from "embla-carousel-react";
import Autoplay from "embla-carousel-autoplay";

const DISHES = ["Osh", "Lag'mon", "Manti"];

export function DishCarousel() {
  const [viewportRef, api] = useEmblaCarousel(
    { loop: true, align: "start" },
    [Autoplay({ delay: 5000, stopOnInteraction: true })],
  );
  const [selected, setSelected] = useState(0);

  useEffect(() => {
    if (!api) return; // birinchi renderda hali yo'q
    const onSelect = () => setSelected(api.selectedScrollSnap());
    onSelect();
    api.on("select", onSelect);
    return () => {
      api.off("select", onSelect);
    };
  }, [api]);

  const prev = useCallback(() => api?.scrollPrev(), [api]);
  const next = useCallback(() => api?.scrollNext(), [api]);

  return (
    <section aria-roledescription="karusel" aria-label="Kun taomlari">
      <div ref={viewportRef} className="embla__viewport">
        <div className="embla__container">
          {DISHES.map((dish, i) => (
            <div
              key={dish}
              className="embla__slide"
              role="group"
              aria-roledescription="slayd"
              aria-label={`${DISHES.length} dan ${i + 1}`}
            >
              {dish}
            </div>
          ))}
        </div>
      </div>
      <button onClick={prev}>Oldingi</button>
      <button onClick={next}>Keyingi</button>
      <p>{selected + 1} / {DISHES.length}</p>
    </section>
  );
}

CSS — Embla bermaydi, uch qoida sizda:

css
.embla__viewport { overflow: hidden; }
.embla__container { display: flex; }
.embla__slide { flex: 0 0 100%; min-width: 0; }

Muhim nuqtalar:

  • viewportRef — ko'rish oynasiga. Embla uning birinchi bolasini (konteyner) suradi va ichidagi har bolani slayd deb hisoblaydi. Tuzilma «Karusel anatomiyasi» bo'limidagi chizmaning aynan o'zi.
  • api birinchi renderda undefined. Embla ref DOM'ga ulangandan keyin yaratiladi. Shuning uchun if (!api) return va api?..
  • Hodisalar — on / off. select — slayd tanlandi, reInit — slaydlar soni yoki sozlama o'zgardi, slidesInView — qaysi slaydlar ko'rinib turibdi. Cleanup'da off — bo'lmasa, har effekt yangi tinglovchi qo'shadi.
  • min-width: 0 — flex elementlari standart holatda o'z ichidagisidan torayolmaydi. Uzun so'zli slayd boshqalarini "itarib" yuboradi. Bu qoida — Embla hujjatidagi asosiy CSS'ning bir qismi.

Embla'ning ko'p ishlatiladigan sozlamalari:

Sozlama Nima qiladi
loop oxiridan keyin boshi (cheksiz)
align "start", "center", "end" — slayd qayerga "yopishadi"
slidesToScroll bir bosishda nechta slayd
dragFree snap'siz erkin surish (lenta uchun)
breakpoints media query bo'yicha boshqa sozlama

API metodlari: scrollNext(), scrollPrev(), scrollTo(i), canScrollNext(), canScrollPrev(), selectedScrollSnap(), scrollSnapList() (nuqtalar soni uchun). Autoplay plagini: api.plugins().autoplay.stop() / .play().

stopOnInteraction: true — foydalanuvchi o'zi surgan yoki tugmani bosganidan keyin avto-aylanish butunlay to'xtaydi. Mantiq oddiy: odam qiziqib qaradi, endi unga xalaqit bermaymiz. Lekin bu to'xtatish tugmasining o'rnini bosmaydi — WCAG 2.2.2 ko'rinadigan tugmani talab qiladi.

shadcn'da Embla qo'lda yozilmaydi — npx shadcn add carousel uni src/ui/carousel.tsx ga ko'chiradi (shadcn/ui I darsidagi oqim, CLI tuzoqlari bilan). Ishlatilishi:

tsx
<Carousel
  opts={{ align: "start", loop: true }}
  plugins={[Autoplay({ delay: 5000 })]}
  setApi={setApi}
>
  <CarouselContent>
    {dishes.map((dish) => (
      <CarouselItem key={dish.id} className="basis-full md:basis-1/3">
        <DishCard dish={dish} />
      </CarouselItem>
    ))}
  </CarouselContent>
  <CarouselPrevious />
  <CarouselNext />
</Carousel>

opts — Embla sozlamalari, plugins — plaginlar, setApi — Embla API'sini tashqariga beradi (nuqtalar yoki "3 / 5" hisoblagich uchun). basis-1/3 — Tailwind: kompyuterda uchta slayd yonma-yon. Komponent role="region", aria-roledescription="carousel", slaydlarga role="group" va chap/o'ng strelkalarni o'zi qo'shadi. Kod sizniki bo'lgani uchun "carousel" ni "karusel" ga almashtirish — bitta qator.

5. Swiper

Swiper — eng eski (2014-yildan) va imkoniyatlari eng ko'p karusellardan biri (14.3.0, 2026-yil sentabr). Embla'dan farqi: tayyor ko'rinish va CSS bilan keladi, imkoniyatlar modullarga bo'lingan — faqat ulaganingiz to'plamga kiradi.

jsx
import { useState } from "react";
import { Swiper, SwiperSlide } from "swiper/react";
import { A11y, Keyboard, Navigation, Thumbs } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/thumbs";

export function Gallery({ photos }) {
  const [thumbs, setThumbs] = useState(null);
  return (
    <>
      <Swiper
        modules={[Navigation, Keyboard, A11y, Thumbs]}
        navigation
        keyboard={{ enabled: true }}
        thumbs={{ swiper: thumbs }}
        breakpoints={{
          0: { slidesPerView: 1 },
          768: { slidesPerView: 2, spaceBetween: 16 },
        }}
      >
        {photos.map((p) => (
          <SwiperSlide key={p.id}>
            <img src={p.src} alt={p.alt} loading="lazy" />
          </SwiperSlide>
        ))}
      </Swiper>
      <Swiper
        onSwiper={setThumbs}
        slidesPerView={4}
        watchSlidesProgress
      >
        {photos.map((p) => (
          <SwiperSlide key={p.id}>
            <img src={p.thumb} alt="" />
          </SwiperSlide>
        ))}
      </Swiper>
    </>
  );
}

Uch g'oya:

  • Modullar — Navigation (strelkalar), Pagination (nuqtalar), Autoplay, Keyboard, A11y (slaydlarga ARIA va e'lonlar), Thumbs, Virtual (minglab slayd) va boshqalar. Ularning CSS'i ham alohida import qilinadi.
  • breakpoints — kalit — ekran eni (px), qiymat — shu endan boshlab sozlama. Telefonda bitta slayd, planshetda ikkita.
  • Thumbs — ikki karusel sinxron: pastdagi kichik rasmlar, tepadagi katta. Pastdagisi onSwiper bilan o'z obyektini beradi, tepadagisi thumbs={{ swiper: thumbs }} bilan unga ulanadi. Bu CSS'da umuman yo'q imkoniyat.

Kichik rasmlarda alt="" — ular bezak: xuddi shu rasm tepada alt bilan bor. Ekran o'quvchi bir rasmni ikki marta eshitmaydi.

Qachon Swiper, qachon Embla? Embla — o'z dizayningiz bo'lsa (shadcn, Tailwind) va hajm muhim bo'lsa. Swiper — tayyor effektlar (fade, cube, coverflow), thumbs, virtual slaydlar va "hammasi bir joyda" kerak bo'lsa. Ikkalasining hajmini bir xil sharoitda o'lchang: kanon faqat Embla'ni o'lchagan (pastda), Swiper'niki o'lchanmagan.

6. Karuseldagi rasmlar

Karusel ko'pincha rasm. Uch qoida (Rasm unumdorligi darsidan, karuselga qo'llab):

jsx
{photos.map((p, i) => (
  <img
    key={p.id}
    src={p.src}
    alt={p.alt}
    width={800}
    height={450}
    loading={i === 0 ? "eager" : "lazy"}
    fetchPriority={i === 0 ? "high" : undefined}
  />
))}
  1. Birinchi slayd — eager va fetchPriority="high". U ko'pincha sahifaning eng katta elementi (LCP). Uni dangasa yuklash LCP'ni sekinlashtiradi. React 19 fetchPriority ni to'g'ri fetchpriority atributiga aylantiradi.
  2. Qolganlari — loading="lazy". Brauzer ularni slayd yaqinlashganda yuklaydi. CSS lentasida (haqiqiy skroll) bu yaxshi ishlaydi. transform bilan suriladigan karuselda (Embla, Swiper) esa brauzer "yaqinlashish" ni har doim ham to'g'ri sezmaydi. Shuning uchun Embla hujjatida alohida "lazy load" misoli bor: slidesInView hodisasi kelganda src qo'yiladi. Swiper'da esa rasm uchun loading="lazy" va slayd ichida yuklanish belgisi tavsiya qilinadi.
  3. width va height (yoki CSS aspect-ratio) — rasm kelguncha joy band bo'lsin. Aks holda rasm kelganda karusel "sakraydi" (CLS).

7. Tajriba: vazifalar da karusel bormi?

Bu darsda vazifalar kodi o'zgarmaydi.

Nega kanonda yo'q. vazifalar — 36rem kenglikdagi bitta ustunli ro'yxat ilovasi. Unda bir vaqtda bittasi ko'rinadigan, galma-gal almashadigan kontent yo'q. Karusel qo'shish — mavzu uchun komponent "o'ylab topish" bo'lardi, foydalanuvchiga esa hech narsa bermasdi. Kanon qoidasi: har qaror o'lchov va ehtiyoj bilan.

Biz sinab ko'rdik. Kanon 25 ta kutubxonani bir xil sharoitda o'lchadi: Vite 8.3.3 build, React 19.3.0, gzip, vazifalar CSP'si ostida Chrome 155 da ochib. Embla tajribasi kodi (tajriba/kitlar/src/k/embla.tsx, kanondan aynan, qatorlar o'qish uchun bo'lingan):

tsx
// #13 Embla Carousel 8.6.0 (shadcn Carousel asosi)
import useEmblaCarousel from "embla-carousel-react";
import { createRoot } from "react-dom/client";
function Karusel() {
  const [ref, api] = useEmblaCarousel({ loop: true });
  return (
    <section aria-roledescription="carousel" aria-label="Taomlar">
      <div ref={ref} style={{ overflow: "hidden" }}>
        <div style={{ display: "flex" }}>
          {["Osh", "Lagmon", "Manti"].map((t, i) => (
            <div
              key={t}
              role="group"
              aria-roledescription="slide"
              aria-label={`${i + 1} / 3`}
              style={{ flex: "0 0 100%" }}
            >
              {t}
            </div>
          ))}
        </div>
      </div>
      <button onClick={() => api?.scrollPrev()}>Oldingi</button>
      <button onClick={() => api?.scrollNext()}>Keyingi</button>
    </section>
  );
}
createRoot(document.getElementById("ilova")!).render(
  <main><h1>Karusel</h1><Karusel /></main>,
);

Natija (natijalar/tajriba/kutubxonalar.txt, aynan; asos — faqat React va react-dom):

text
asos            JS   67719 (+   0.0 KB) CSS      0 | CSP 0 | xato: yo'q
embla           JS   75571 (+   7.7 KB) CSS      0 | CSP 0 | xato: yo'q

O'qiymiz:

  • +7.7 KB gzip — Embla va uning React qobig'i. Bu vazifalar dagi Radix menyusidan (+27.2 KB) uch baravar kam, lucide ikonkalaridan (+2.0) to'rt baravar ko'p.
  • CSS 0 — Embla hech qanday stil fayli bermaydi, hammasi sizniki.
  • CSP 0 — style-src 'self' va Trusted Types ostida bitta ham buzilish yo'q. Embla konteynerni element.style.transform orqali suradi — bu CSSOM, CSP unga ruxsat beradi (Radix chuqur darsidagi farq: <style> tegi — yo'q, style xususiyati — bor).

Tajriba kodidagi aria-roledescription="carousel" — inglizcha, chunki tajriba faqat hajm va CSP'ni o'lchaydi. O'zbekcha ilovada «karusel» deb yozing.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
«Oldingi» birinchi slaydda bo'sh ekran i % count manfiy son beradi (i + count) % count
strelka bilan matn maydonida yozib bo'lmaydi keydown window ga ulangan faqat karusel section iga
avto-aylanish to'xtamaydi to'xtatish tugmasi yo'q tugma + hover/fokus pauza
sahifadan chiqqach konsolda xato yoki sekinlik interval cleanup'siz return () => clearInterval(id)
taymer har slaydda "noldan" boshlanadi effekt bog'liqligida har render yangi funksiya latest ref (tick.current)
Malika har 5 soniyada gapni eshitadi aylanayotganda aria-live="polite" aylanayotganda "off"
birinchi rasm kech chiqadi (LCP yomon) hamma rasm loading="lazy" birinchisi eager + fetchPriority="high"
uzun matnli slayd boshqalarini itaradi flex slaydda min-width: 0 yo'q flex: 0 0 100%; min-width: 0
Embla api — undefined xatosi birinchi renderda API hali yo'q if (!api) return, api?.

9. Mashqlar

Mashqlar kurs/mashqlar/20/13-karusel/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): Embla API'si

Embla'da qaysi nom kerak?

  • Keyingi slaydga o'tish metodi →
  • Hozirgi slayd raqamini qaytaradigan metod →
  • Slayd almashganini bildiradigan hodisa nomi →
  • Oxiridan keyin boshiga qaytish sozlamasi →
Yechim
  • scrollNext() (orqaga — scrollPrev(), istalganiga — scrollTo(i)).
  • selectedScrollSnap() — 0 dan boshlanadi; ekranga + 1 qilib chiqaring.
  • "select" — api.on("select", fn), cleanup'da api.off("select", fn).
  • loop: true — useEmblaCarousel({ loop: true }).

2-mashq (o'rta): nuqtalar

«Bitta state — index» bo'limidagi karuselga nuqtalar qo'shing: har slayd uchun bitta tugma, matni raqam (1, 2, 3), aria-label — «1-slaydga o'tish». Faol nuqtada aria-current="true", qolganlarida atribut yo'q.

Ishora: nuqtalar — DISHES.map((_, i) => …); bosilganda setIndex(i). aria-current={i === index ? "true" : undefined} — undefined bo'lsa, React atributni umuman yozmaydi.

Yechim
jsx
import { useEffect, useState } from "react";

const DISHES = ["Osh", "Lag'mon", "Manti"];

export default function App() {
  const [index, setIndex] = useState(0);
  const count = DISHES.length;
  useEffect(() => {
    const current = document.querySelector("[aria-current]");
    console.log("faol nuqta:", current?.textContent);
  }, [index]);
  return (
    <section aria-roledescription="karusel" aria-label="Kun taomlari">
      <div aria-live="polite">
        {DISHES.map((dish, i) => (
          <div
            key={dish}
            role="group"
            aria-roledescription="slayd"
            aria-label={`${count} dan ${i + 1}`}
            hidden={i !== index}
          >
            {dish}
          </div>
        ))}
      </div>
      <div>
        {DISHES.map((dish, i) => (
          <button
            key={dish}
            aria-label={`${i + 1}-slaydga o'tish`}
            aria-current={i === index ? "true" : undefined}
            onClick={() => setIndex(i)}
          >
            {i + 1}
          </button>
        ))}
      </div>
    </section>
  );
}

Konsolda:

text
faol nuqta: 1
faol nuqta: 3

Ekranda:

text
Manti
123

Ekranda 123 — uchta tugma matni yonma-yon yozilgan. Natija oynasida esa ular uchta alohida tugma. Tugmaning nomi — aria-label. Malika uni shu nom bilan eshitadi, "3, tugma" emas. aria-current — "ro'yxatdagi hozirgi element": ekran o'quvchi faol nuqtada "joriy" deb qo'shib aytadi.

3-mashq (qiyin): halqasiz karusel va o'zi to'xtaydigan avto-aylanish

«Bahor» aksiyalari karuseli. Talablar:

  1. Halqa yo'q: birinchi slaydda «Oldingi» disabled, oxirgisida «Keyingi» disabled.
  2. Avto-aylanish 500 ms (darsda) — oxirgi slaydga yetganda o'zi to'xtaydi va konsolga avto-aylanish to'xtadi yozadi.
  3. Har slayd almashganda konsolga slayd: N (birinchi chizishdan tashqari).
  4. Foydalanuvchi «Oldingi»/«Keyingi» ni bossa ham avto-aylanish to'xtaydi (Embla'ning stopOnInteraction i kabi).

Ishora: «Avto-aylanish: qoidalar kodda» bo'limidagi useAutoplay ni oling. Avtomatik qadamda setIndex ga funksiya bering (yangilovchi funksiya) va oxirgi slaydda setPlaying(false) ni chaqiring.

Yechim
jsx
import { useEffect, useRef, useState } from "react";

const PROMOS = ["-10% osh", "Manti + choy", "Bepul yetkazish"];

function useAutoplay(onTick, delay, playing) {
  const tick = useRef(onTick);
  tick.current = onTick;
  useEffect(() => {
    if (!playing) return;
    const id = setInterval(() => tick.current(), delay);
    return () => clearInterval(id);
  }, [delay, playing]);
}

export default function App() {
  const [index, setIndex] = useState(0);
  const [playing, setPlaying] = useState(true);
  const last = PROMOS.length - 1;
  useAutoplay(
    () => {
      const next = Math.min(index + 1, last);
      setIndex(next);
      if (next === last) {
        setPlaying(false);
        console.log("avto-aylanish to'xtadi");
      }
    },
    500,
    playing,
  );
  const first = useRef(true);
  useEffect(() => {
    if (first.current) {
      first.current = false;
      return;
    }
    console.log("slayd:", index + 1);
  }, [index]);
  function go(i) {
    setPlaying(false); // foydalanuvchi aralashdi
    setIndex(i);
  }
  return (
    <section aria-roledescription="karusel" aria-label="Aksiyalar">
      <p>{PROMOS[index]}</p>
      <button disabled={index === 0} onClick={() => go(index - 1)}>
        Oldingi
      </button>{" "}
      <button disabled={index === last} onClick={() => go(index + 1)}>
        Keyingi
      </button>
    </section>
  );
}

Konsolda:

text
slayd: 2
avto-aylanish to'xtadi
slayd: 3

Ekranda:

text
Bepul yetkazish
Oldingi Keyingi

Konsol tartibiga qarang: avto-aylanish to'xtadi slayd: 3 dan oldin. Taymer funksiyasi setIndex(2) ni chaqirdi, darhol console.log ham ishladi. slayd: 3 esa effektdan — React yangi holatni chizib bo'lgandan keyin. Hodisa ichidagi kod va effekt orasidagi tartib — useEffect darsidagi qoida.

Bu yerda tick.current dagi funksiya index ni to'g'ridan-to'g'ri o'qiydi — u har renderda yangilanadi (latest ref), shuning uchun eskirgan qiymat muammosi yo'q. «Keyingi» oxirgi slaydda disabled — Bobur Tab bilan uni o'tkazib yuboradi, Malika "o'chirilgan" deb eshitadi.

4-mashq: o'lchovni o'zingiz takrorlang

O'z Vite loyihangizda (16-qismdagi shablon) ikki build qiling: avval faqat React bilan bitta sahifa, keyin «Embla Carousel» bo'limidagi DishCarousel bilan. npm run build chiqishidagi gzip ustunini solishtiring.

Yechim

Tekshiruv ro'yxati:

  • npm i -E embla-carousel-react@8.6.0 — versiya aniq (-E), package.json da ^ yo'q.
  • Build chiqishida JS chunk'ining gzip farqi ~7–8 KB bo'lishi kerak (kanon: +7.7 KB; Autoplay plagini bilan biroz ko'proq). Farq juda katta bo'lsa — Embla'dan tashqari nima qo'shilganini tekshiring.
  • DevTools → Console: CSP o'rnatilgan bo'lsa ham Applying inline style violates… xatosi yo'q.
  • Klaviatura: Tab bilan «Oldingi»/«Keyingi» ga yetib boriladi, Enter ishlaydi.

10. Real ishda

  • Onlayn do'konlar va yetkazib berish ilovalari — mahsulot rasmlari galereyasi (Swiper thumbs bilan), "Sizga yoqishi mumkin" lentasi (ko'pincha oddiy scroll-snap). Bosh sahifadagi katta "banner karusel" esa ko'p jamoalarda olib tashlanmoqda — kliklar asosan birinchi slaydga tushadi.
  • shadcn loyihalarida — Carousel (Embla) tayyor. Uni o'zgartirish — src/ui/carousel.tsx ni tahrirlash.
  • Intervyu savollari: "Karuselni a11y uchun qanday qilasiz?", "Avto-aylanish qachon mumkin?", "Embla va Swiper farqi?", "Karuseldagi rasmlarni qanday yuklaysiz?", "setInterval ni React'da qanday to'g'ri ishlatasiz?".

Xulosa

  • Avval savol: karusel kerakmi yoki CSS lentasi yetadimi (CSS karusel — 0 KB JS).
  • O'z karuselingiz: bitta index state, (i + count) % count halqasi, keydown faqat karuselda, role="group" va o'zbekcha aria-roledescription.
  • Avto-aylanish: to'xtatish tugmasi birinchi, hover/fokusda pauza, prefers-reduced-motion da o'chiq, aylanayotganda aria-live="off", interval — cleanup bilan.
  • Embla: [ref, api], on/off hodisalari, Autoplay plagini, CSS sizniki; shadcn Carousel — uning ustida. O'lchov: +7.7 KB, CSP 0.
  • Swiper: modullar, breakpoints, thumbs, tayyor effektlar.
  • Rasmlar: birinchisi eager + fetchPriority="high", qolgani lazy, o'lcham oldindan.

Keyingi dars: Jadvallar I: TanStack Table asoslari — headless jadval: ustun ta'riflari, saralash, filtr va sahifalash TanStack Table v9 (useTable) bilan; vazifalar ning teglar jadvali sarlavhasini bosib saralanadi.

Manbalar

  • Embla Carousel: Get started (React), API, Plugins → Autoplay, Examples → Lazy load — embla-carousel.com (8.6.0)
  • Swiper React Components, Modules — swiperjs.com/react (14.3.0)
  • shadcn/ui: Carousel — ui.shadcn.com/docs/components/carousel
  • WAI-ARIA APG: Carousel Pattern — w3.org/WAI/ARIA/apg/patterns/carousel
  • W3C: Understanding Success Criterion 2.2.2: Pause, Stop, Hide — w3.org/WAI/WCAG22/Understanding
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Karusel va slayderlar React'da: o'z karuselingiz, Embla va Swiper, avto-aylanish va a11y — IlmHamroh