Mundarija (24)
- Bu darsda
- 1. Nega bu kerak?
- 2. Karusel anatomiyasi
- 3. React'da o'z karuselingiz
- 3.1 Bitta state — index
- 3.2 Avto-aylanish: qoidalar kodda
- 3.3 Gibrid: CSS lenta va React tugmalari
- 3.4 Barmoq bilan surish: pointer hodisalari
- 4. Embla Carousel
- 4.1 Nega Embla
- 4.2 Asosiy misol
- 4.3 shadcn Carousel
- 5. Swiper
- 6. Karuseldagi rasmlar
- 7. Tajriba: vazifalar da karusel bormi?
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): Embla API'si
- 2-mashq (o'rta): nuqtalar
- 3-mashq (qiyin): halqasiz karusel va o'zi to'xtaydigan avto-aylanish
- 4-mashq: o'lchovni o'zingiz takrorlang
- 10. Real ishda
- Xulosa
- Manbalar
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
indexstate, «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-motionda — 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
vazifalarda 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:
- "Kun taomlari" — uchta katta rasm, bittadan ko'rinsin, o'zi almashsin.
- Telefonda barmoq bilan surilsin, kompyuterda sichqoncha bilan "tortib" ham.
- 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):
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:
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—-1ni ham to'g'ri aylantiradi:(-1 + 3) % 3 = 2. Faqati % countyozilsa, JavaScript'da-1 % 3→-1va 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—sectionda. 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}vakey={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:
- To'xtatish tugmasi — karuseldan oldin, Tab bilan birinchi.
- Sichqoncha ustida yoki fokus ichida — pauza.
prefers-reduced-motion: reduce— umuman yoqilmaydi.- 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.
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:
slayd: 2Ekranda:
Davom ettirish
Lag'mon500 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 holdaonTickhar 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:matchMediafaqat birinchi renderda bir marta chaqiriladi.
Diqqat:
onFocus/onBlurReact'da ichki elementlardan ham ko'tariladi (DOM'dagifocusin/focusoutkabi). Shuning uchun fokus karusel ichidagi istalgan tugmada turganda pauza ishlaydi. Lekin fokus bir tugmadan qo'shni tugmaga o'tgandablurvafocusketma-ket keladi — oraliqda bir lahzafocus = 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.
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:
scrollLeft: 531
scrollLeft: 1022Raqamlar 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, faqatno-preferenceda.scrollBygabehaviorbermadik: brauzer CSS'ga qaraydi. Harakatni kamaytirish so'ralganda lenta silliq emas, darhol suriladi — JavaScript'damatchMediatekshiruvi shart emas.tabIndex={0}— lentaga Tab bilan kelib, strelkalar bilan surish mumkin. Skrollanadigan hudud klaviaturaga ochiq bo'lishi kerak (axe buniscrollable-region-focusableqoidasi bilan tekshiradi).- Snap o'zi to'g'rilaydi.
stepbir 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.
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):
dx: -120Ekranda «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,pointercancelyuboradi vapointerupkelmaydi.setPointerCapture— barmoq elementdan chiqib ketsa hampointerupshu 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. Embla Carousel
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:
npm i -E embla-carousel-react@8.6.0 embla-carousel-autoplay@8.6.04.2 Asosiy misol
Embla ikki narsa qaytaradi: DOM uchun ref va boshqaruv uchun API obyekti:
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:
.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.apibirinchi renderdaundefined. Embla ref DOM'ga ulangandan keyin yaratiladi. Shuning uchunif (!api) returnvaapi?..- Hodisalar —
on/off.select— slayd tanlandi,reInit— slaydlar soni yoki sozlama o'zgardi,slidesInView— qaysi slaydlar ko'rinib turibdi. Cleanup'daoff— 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.
4.3 shadcn Carousel
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:
<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.
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
onSwiperbilan o'z obyektini beradi, tepadagisithumbs={{ 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):
{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}
/>
))}- Birinchi slayd —
eagervafetchPriority="high". U ko'pincha sahifaning eng katta elementi (LCP). Uni dangasa yuklash LCP'ni sekinlashtiradi. React 19fetchPriorityni to'g'rifetchpriorityatributiga aylantiradi. - Qolganlari —
loading="lazy". Brauzer ularni slayd yaqinlashganda yuklaydi. CSS lentasida (haqiqiy skroll) bu yaxshi ishlaydi.transformbilan suriladigan karuselda (Embla, Swiper) esa brauzer "yaqinlashish" ni har doim ham to'g'ri sezmaydi. Shuning uchun Embla hujjatida alohida "lazy load" misoli bor:slidesInViewhodisasi kelgandasrcqo'yiladi. Swiper'da esa rasm uchunloading="lazy"va slayd ichida yuklanish belgisi tavsiya qilinadi. widthvaheight(yoki CSSaspect-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):
// #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):
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'qO'qiymiz:
- +7.7 KB gzip — Embla va uning React qobig'i. Bu
vazifalardagi 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 konteynernielement.style.transformorqali suradi — bu CSSOM, CSP unga ruxsat beradi (Radix chuqur darsidagi farq:<style>tegi — yo'q,stylexususiyati — 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+ 1qilib chiqaring."select"—api.on("select", fn), cleanup'daapi.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
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:
faol nuqta: 1
faol nuqta: 3Ekranda:
Manti
123Ekranda 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:
- Halqa yo'q: birinchi slaydda «Oldingi»
disabled, oxirgisida «Keyingi»disabled. - Avto-aylanish 500 ms (darsda) — oxirgi slaydga yetganda o'zi to'xtaydi va konsolga
avto-aylanish to'xtadiyozadi. - Har slayd almashganda konsolga
slayd: N(birinchi chizishdan tashqari). - Foydalanuvchi «Oldingi»/«Keyingi» ni bossa ham avto-aylanish to'xtaydi (Embla'ning
stopOnInteractioni 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
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:
slayd: 2
avto-aylanish to'xtadi
slayd: 3Ekranda:
Bepul yetkazish
Oldingi KeyingiKonsol 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.jsonda^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.tsxni tahrirlash. - Intervyu savollari: "Karuselni a11y uchun qanday qilasiz?", "Avto-aylanish qachon mumkin?", "Embla va Swiper farqi?", "Karuseldagi rasmlarni qanday yuklaysiz?", "
setIntervalni 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
indexstate,(i + count) % counthalqasi,keydownfaqat karuselda,role="group"va o'zbekchaaria-roledescription. - Avto-aylanish: to'xtatish tugmasi birinchi, hover/fokusda pauza,
prefers-reduced-motionda o'chiq, aylanayotgandaaria-live="off", interval — cleanup bilan. - Embla:
[ref, api],on/offhodisalari, 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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!