IlmHamroh
JavaScript Full-stack/13-qism. Brauzer API'lari, performans va xavfsizlik5/32-dars23 daqiqa
Mundarija (39)

JavaScript matchMedia: media query'lar va foydalanuvchi sozlamalari

Qisqacha: matchMedia("(min-width: 40rem)") — CSS'dagi @media savolini JavaScript'dan beradi. U MediaQueryList obyektini qaytaradi: .matches — shart hozir bajarilyaptimi (true/false), change hodisasi — javob o'zgargan paytda keladi. Shu bilan tizimning qorong'i rejimi (prefers-color-scheme) va "harakatni kamaytirish" (prefers-reduced-motion) sozlamasiga JavaScript'dan moslashasiz.

Bu darsda

  • matchMedia bilan istalgan media query'ni JavaScript'dan tekshira olasiz va matches, media xususiyatlarini o'qiysiz.
  • change hodisasini resize dan farqlaysiz: u faqat javob almashganda keladi.
  • Tizim/yorug'/qorong'i tanlovli temani yozasiz: tizim o'zgarishi jonli kuzatiladi, telefon paneli rangi ham moslashadi.
  • "Harakatni kamaytirish" sozlamasini JavaScript animatsiyalarida hurmat qilasiz.
  • vazifalar ga tema tanlovini qo'shasiz.

Oldin bilishingiz kerak: Qorong'i rejim va temalash, prefers-reduced-motion va harakat etikasi, Fokus, scroll va resize hodisalari, localStorage va sessionStorage, requestAnimationFrame.

1. Nega bu kerak?

O'tgan darsda ResizeObserver bilan element o'lchamini kuzatdik. Bugun boshqa savol: ekran va foydalanuvchi haqida.

CSS'da bunga javob bor — media query. @media (min-width: 40rem) keng ekranda boshqa stil beradi, @media (prefers-color-scheme: dark) qorong'i rejimda. Ko'rinish o'zgarishi uchun shu yetadi. Lekin ba'zi qarorlarni CSS qila olmaydi:

  • «Bahor» bosh sahifasida taomlar karuseli o'zi aylanadi. "Harakatni kamaytirish" yoqilgan mehmonga avto-aylanish kerak emas. Aylanishni JavaScript boshqaradi — demak u sozlamani bilishi kerak.
  • Telefonda brauzerning yuqori paneli <meta name="theme-color"> rangida bo'yaladi. Mehmon qo'lda "Qorong'i" ni tanlasa, sahifa qorong'i bo'ladi, panel esa yorug' qoladi. Meta tegni faqat JavaScript o'zgartira oladi.
  • <canvas> dagi grafik CSS'ni tushunmaydi. Qorong'i rejimda chiziq rangini JavaScript tanlaydi.
  • Keng ekranda yon panel ochiq turadi, tor ekranda — yopiq. Bu holat JavaScript'dagi o'zgaruvchida saqlanadi.

Fokus, scroll va resize darsida aytgan edik: "Ekran 40rem dan kengmi?" degan savolga resize siz javob beradigan vosita bor — CSS media query'ning JavaScript'dagi egizagi. U — matchMedia.

2. matchMedia asoslari

2.1 Birinchi savol

js
const wide = matchMedia("(min-width: 40rem)");

console.log(wide.matches); // true yoki false — ekraningizga qarab
console.log(wide.media); // (min-width: 40rem)

matchMedia ga CSS'dagi @media dan keyin yoziladigan matnni berasiz — aynan shu sintaksis. U MediaQueryList obyektini qaytaradi. "Ro'yxat" so'zidan cho'chimang: bu bitta savolning javobi. Unda ikki asosiy xususiyat bor:

  • matches — shart hozir bajarilyaptimi. Oyna 40rem (640 px) dan keng bo'lsa — true.
  • media — siz bergan savol, brauzer tushungan shaklda.

matchMedia — window ning metodi, xuddi setTimeout kabi. Shuning uchun window.matchMedia(...) ham, qisqa matchMedia(...) ham bir xil ishlaydi.

2.2 media nimani ko'rsatadi

Chrome 154 da sinab ko'rdik. matchMedia("(min-width:40rem)").media — "(min-width: 40rem)": brauzer bo'sh joyni o'zi qo'ydi. Savolni tushunmasa-chi? Qavslar unutilgan matchMedia("min-width: 600px") ning media si — "not all", matches esa doim false. Ya'ni "hech qaysi ekranga mos emas".

Bu muhim tuzoq. Xato sintaksis xato bermaydi. Kod jim ishlaydi, faqat shart hech qachon bajarilmaydi. Shubha tug'ilsa — media ni konsolga chiqaring. "not all" ko'rsangiz, qavslarni tekshiring.

Yana bir nozik joy: brauzer tanimaydigan xususiyat (masalan, (foo: bar)) "not all" ga aylanmaydi — media o'zgarmay qoladi, matches — false. Demak media ga qarab "brauzer bu xususiyatni biladimi?" deb xulosa chiqarib bo'lmaydi.

2.3 Zamonaviy diapazon sintaksisi

Foydalanuvchi afzalliklari darsidagi barcha media query'lar shu yerda ham ishlaydi, jumladan yangi diapazon yozuvi:

js
matchMedia("(width >= 40rem)").matches; // min-width bilan bir xil
matchMedia("(400px <= width <= 700px)").matches; // oraliq
matchMedia("(orientation: portrait)").matches; // tik ekran
matchMedia("(hover: hover) and (pointer: fine)").matches; // kursor

Oxirgisi qiziq: "sichqonchasi bor qurilmami?" — (hover: hover) (kursor elementlar ustida "suza" oladi) va (pointer: fine) (aniq ko'rsatkich). Telefon barmog'i — (pointer: coarse), ya'ni "qo'pol". Bu "telefonmi?" savoliga ekran kengligidan ko'ra to'g'riroq javob: katta planshet ham barmoq bilan boshqariladi.

Tekshirib ko'ring: Sardor matchMedia("max-width: 600px") yozdi. Telefonda ham, kompyuterda ham matches — false. Nega? Qanday tekshirasiz?

Javob

Qavslar yo'q — brauzer savolni tushunmadi va uni "not all" ("hech qaysi ekran") deb qabul qildi. Xato chiqmaydi, shuning uchun sezish qiyin. console.log(mq.media) — not all ko'rinadi. To'g'risi: matchMedia("(max-width: 600px)").

3. change hodisasi: javob o'zgarganda

3.1 Bir marta tekshirish yetmaydi

matches — faqat hozirgi javob. Foydalanuvchi telefonni yotqizdi, oynani toraytirdi yoki tizimni qorong'i rejimga o'tkazdi — eski javob eskirdi. Yangi javobni bilish uchun MediaQueryList ning change hodisasini tinglaymiz:

js
const wide = matchMedia("(min-width: 40rem)");

wide.addEventListener("change", (event) => {
  console.log(event.matches ? "keng ekran" : "tor ekran");
});

Hodisa obyekti — MediaQueryListEvent. Unda ham matches (yangi javob) va media (savol) bor. event.matches va wide.matches — bir xil qiymat, qaysi biri qulay bo'lsa, shunisini o'qing.

3.2 resize bilan taqqoslash

Fokus, scroll va resize darsida resize ni debounce qilgan edik — u oyna har piksel o'zgarganda keladi. change esa faqat chegaradan o'tganda keladi. Ko'rib chiqing:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  #mode { font-size: 1.5rem; font-weight: 700; margin: 0; }
  .wide #mode { color: #1f6f4a; }
</style>
<p>Ekran: <span id="mode">…</span></p>
<p>resize hodisalari: <b id="resizes">0</b>,
   change hodisalari: <b id="changes">0</b></p>
<script>
  const wide = matchMedia("(min-width: 40rem)");
  const mode = document.getElementById("mode");
  let resizes = 0;
  let changes = 0;

  function show(isWide) {
    mode.textContent = isWide ? "keng (≥ 40rem)" : "tor (< 40rem)";
    document.body.classList.toggle("wide", isWide);
  }

  addEventListener("resize", () => {
    resizes++;
    document.getElementById("resizes").textContent = resizes;
  });

  wide.addEventListener("change", (event) => {
    changes++;
    document.getElementById("changes").textContent = changes;
    show(event.matches);
    console.log(`change: ${event.matches ? "keng" : "tor"}`);
  });

  show(wide.matches); // boshlang'ich holat — bir marta qo'lda
</script>

Oyna ostidagi 360, 768, 1280 tugmalarini bosing. Ekran 640 px chegarasidan o'tgandagina konsolga change: tor yoki change: keng chiqadi. Biz Chrome 154 da oyna kengligini 700 → 600 → 500 → 300 → 900 px qilib sinadik: resize — 5 marta, change — 2 marta (tor, keyin keng). 500 va 300 da chegara kesib o'tilmadi — hodisa ham yo'q.

Oxirgi qatorga e'tibor bering: show(wide.matches). change faqat o'zgarishda keladi, sahifa ochilganda emas. Boshlang'ich holatni o'zingiz bir marta qo'llashingiz kerak. Bu ko'p uchraydigan xato — "Ko'p uchraydigan xatolar" bo'limida yana bor.

Konsolga sahifa ochilganda hech narsa chiqmaydi — ataylab. Boshlang'ich javob ekraningizga bog'liq: telefonda bir xil, kompyuterda boshqa.

3.3 Eski usul: addListener

Eski kodda shunday yozuvni uchratasiz:

js
wide.addListener((event) => console.log(event.matches));

addListener va removeListener — eskirgan (deprecated) metodlar. Ular hali ishlaydi (Chrome 154 da sinadik — change bilan birga chaqirildi), lekin yangi kodda addEventListener("change", ...) yozing. Eski metodlar 2020-yilgacha Safari uchun kerak edi: u MediaQueryList da addEventListener ni 14-versiyadan boshlab qo'llagan.

Tinglashni to'xtatish ham odatdagidek: removeEventListener("change", handler) yoki Listener opsiyalari darsidagi signal bilan.

Tekshirib ko'ring: Oyna 1000 px dan 300 px gacha sekin toraytirildi. (min-width: 40rem) uchun change necha marta keladi? resize chi?

Javob

change — bir marta: 640 px chegarasidan o'tganda (matches true dan false ga). resize esa ko'p marta — brauzer oyna o'lchamini har yangilaganda. Shuning uchun "qaysi rejimdaman?" savoliga change tejamkor javob: debounce ham kerak emas.

4. Foydalanuvchi sozlamalari

4.1 Tizim nimani aytadi

Telefon va kompyuter sozlamalarida odam o'zi uchun muhim narsalarni tanlaydi: qorong'i rejim, harakatni kamaytirish, yuqori kontrast. Brauzer bularni sahifaga foydalanuvchi afzalliklari (user preferences) media xususiyatlari orqali aytadi. CSS'da ularni Foydalanuvchi afzalliklari darsida ishlatgansiz — JavaScript'da xuddi shu nomlar:

Media xususiyat Nima deydi
(prefers-color-scheme: dark) tizim qorong'i rejimda
(prefers-reduced-motion: reduce) harakatni kamaytirish yoqilgan
(prefers-contrast: more) yuqori kontrast so'ralgan
(forced-colors: active) tizim ranglarni o'zi majburlayapti (Windows kontrast temalari)

Chrome 154 bularning hammasini tushunadi. Yana biri — (prefers-reduced-transparency: reduce), "shaffoflikni kamaytirish". Uni hamma brauzer ham bilmaydi: Chrome va Edge'da bor, Firefox va Safari'da hali yo'q (web-features, 2026-yil oktabr).

4.2 Sozlamani DevTools'da sinash

Har safar tizim sozlamasini o'zgartirish noqulay. Chrome DevTools buni o'zi "soxtalashtiradi": Ctrl + Shift + P → Rendering deb yozing → Emulate CSS media feature prefers-color-scheme ro'yxatidan dark ni tanlang. Sahifa darhol qorong'i rejimda deb hisoblaydi va change hodisasi ham keladi. Xuddi shu panelda prefers-reduced-motion, prefers-contrast va forced-colors ham bor.

Biz darsdagi misollarni aynan shu emulyatsiya orqali sinadik: prefers-color-scheme ni dark ga, keyin prefers-reduced-motion ni reduce ga o'tkazganda ikkala change tinglovchisi ham ishladi.

5. Tema: tizim, yorug' yoki qorong'i

5.1 Uch holat

Qorong'i rejim darsida «Bahor»ning CSS tomonini qurgan edik. Ranglar light-dark() bilan yozilgan, <html data-tema="qorongi"> esa tizimga qaramay qorong'i rejim beradi. <head> dagi kichik tema.js saqlangan tanlovni sahifa chizilishidan oldin qo'yadi. Footer'da esa uch radio tugma bor: Tizim, Yorug', Qorong'i — hidden holida. O'shanda "tugmalar kodini matchMedia darsida yozamiz" degan edik. Mana u dars.

Mantiq oddiy:

flowchart TD
  A["Tanlov: tizim / yorug / qorongi"] --> B{"tizim?"}
  B -- yo'q --> C["data-tema = tanlov"]
  B -- ha --> D["data-tema yo'q"]
  D --> E["CSS: prefers-color-scheme"]
  C --> F["Ko'rinadigan tema"]
  E --> F
  F --> G["theme-color meta"]
  H["Tizim o'zgardi: change"] --> F

Ranglarni CSS o'zi almashtiradi. JavaScript'ning ishi uchta: tanlovni saqlash, data-tema ni qo'yish yoki olib tashlash va ko'rinadigan temani hisoblab, telefon paneli rangini moslash. "Tizim" tanlangan bo'lsa, ko'rinadigan tema tizimga bog'liq — uni matchMedia aytadi.

5.2 Jonli misol

html
<style>
  :root { color-scheme: light dark; }
  :root[data-tema="yorug"] { color-scheme: light; }
  :root[data-tema="qorongi"] { color-scheme: dark; }
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 0;
    padding: 1rem; min-height: 9rem;
    background: light-dark(#f7faf8, #14201a);
    color: light-dark(#1f2a24, #e6efe9); }
  fieldset { border: 1px solid light-dark(#c9d6cf, #3a4d43);
    border-radius: 0.5rem; }
</style>
<fieldset id="theme-choice" hidden>
  <legend>Tema</legend>
  <input type="radio" name="tema" id="tema-tizim" value="tizim">
  <label for="tema-tizim">Tizim</label>
  <input type="radio" name="tema" id="tema-yorug" value="yorug">
  <label for="tema-yorug">Yorug'</label>
  <input type="radio" name="tema" id="tema-qorongi" value="qorongi">
  <label for="tema-qorongi">Qorong'i</label>
</fieldset>
<p>Ko'rinadigan tema: <b id="visible">…</b></p>
<script>
  const THEMES = ["tizim", "yorug", "qorongi"];
  const group = document.getElementById("theme-choice");
  const systemDark = matchMedia("(prefers-color-scheme: dark)");
  const saved = localStorage.getItem("tema");
  let choice = THEMES.includes(saved) ? saved : "tizim";

  function visibleTheme() {
    if (choice !== "tizim") return choice;
    return systemDark.matches ? "qorongi" : "yorug";
  }

  function apply() {
    const html = document.documentElement;
    if (choice === "tizim") delete html.dataset.tema;
    else html.dataset.tema = choice;
    document.getElementById("visible").textContent = visibleTheme();
    group.elements.namedItem(`tema-${choice}`).checked = true;
  }

  group.addEventListener("change", (event) => {
    choice = event.target.value;
    if (choice === "tizim") localStorage.removeItem("tema");
    else localStorage.setItem("tema", choice);
    apply();
    console.log(`Tanlov: ${choice}, ko'rinadi: ${visibleTheme()}`);
  });
  systemDark.addEventListener("change", apply);

  apply();
  group.hidden = false;
</script>

«Qorong'i» ni bosing — oyna qorong'i bo'ladi va konsolda Tanlov: qorongi, ko'rinadi: qorongi. «Yorug'» — Tanlov: yorug, ko'rinadi: yorug. «Tizim» — ko'rinadigan tema kompyuteringiz sozlamasiga qaytadi. Uchala tugmani bosib tekshirdik (tizim yorug' holatida): oxirgisi Tanlov: tizim, ko'rinadi: yorug berdi.

Natija oynasida localStorage haqiqiy emas — sayt uning o'rinbosarini beradi, tanlov shu blok uchun saqlanadi. «Qayta ishga tushirish» ni bosing: oxirgi tanlov joyida. Haqiqiy saytingizda — brauzerning o'z xotirasi.

5.3 Kod qatorma-qator

  • systemDark — bir marta yaratiladi va doim "tirik": uning matches i har o'qilganda tizimning hozirgi javobini beradi.
  • THEMES.includes(saved) — xotiradagi qiymatga ishonmaymiz. Kimdir DevTools'da "dark" yozib qo'ygan bo'lishi mumkin — unda "tizim". Bu oq ro'yxat g'oyasi.
  • visibleTheme() — tanlov "tizim" bo'lmasa, tanlovning o'zi. "Tizim" bo'lsa — matchMedia javobi.
  • group.elements.namedItem(`tema-${choice}`) — fieldset ning elements ro'yxatidan maydonni id (yoki name) bo'yicha topadi. checked = true — saqlangan tanlovni ekranda ham belgilaydi.
  • "tizim" tanlansa, xotiradagi yozuv o'chiriladi, "tizim" deb yozilmaydi. Yozuv yo'q — tanlov yo'q — tizim ishlaydi. <head> dagi tema.js ham shunday o'ylaydi.
  • systemDark.addEventListener("change", apply) — "Tizim" tanlangan paytda kompyuter kechki rejimga o'tsa, sahifa ham darhol o'tadi. CSS buni o'zi qiladi, lekin #visible matni va telefon paneli rangi — JavaScript ishi.
  • group.hidden = false — eng oxirida. JavaScript ishlamasa, tugmalar yashirin qoladi: ishlamaydigan tugmani ko'rsatish mehmonni aldash bo'lardi.

5.4 Telefon paneli: theme-color

Qorong'i rejim darsidagi ogohlantirishni eslang: media li ikkita <meta name="theme-color"> faqat tizimga qaraydi. Tizimi yorug' mehmon «Qorong'i» ni tanlasa, sahifa qorong'i, panel esa oq qoladi. Yechim — bitta meta teg va uning content ini JavaScript yangilaydi:

js
const PANEL_COLORS = { yorug: "#f7faf8", qorongi: "#14201a" };
const panel = document.querySelector('meta[name="theme-color"]');

// apply() ichiga, ko'rinadigan tema hisoblangandan keyin:
panel.content = PANEL_COLORS[visibleTheme()];

Rang — sahifa fonining o'zi: panel va sahifa bir butun bo'lib ko'rinadi. apply() tizim o'zgarishida ham chaqirilgani uchun panel "Tizim" rejimida ham to'g'ri qoladi.

5.5 «Bahor» va portfolio

«Bahor»ning assets/js/tema-tanlash.js fayli — yuqoridagi kodning o'zi, faqat radio tugmalar footer'da va theme-color qo'shilgan. Forma komponentlari darsida "tanlovni eslab qolish — tema-tanlash.js ishi" degan edik — endi siz uni o'qiy olasiz. Yakuniy landing loyihasidagi tugmali variant ham xuddi shu uch qadamni qiladi: guruhni ochadi, tanlovni belgilaydi, saqlaydi.

Portfolio'ngizdagi bitta "Qorong'i rejim" tugmasiga ham bitta tuzatish kerak. addEventListener asoslari darsida tugma aria-pressed="false" bilan boshlanardi. Tizimi qorong'i odam uchun bu yolg'on: sahifa allaqachon qorong'i. Boshlang'ich holatni tizimdan oling: aria-pressed = matchMedia("(prefers-color-scheme: dark)").matches, va tema:ozgardi hodisasi tizim change ida ham yuborilsin.

Tekshirib ko'ring: Mehmon «Yorug'» ni tanladi. Keyin kompyuteri kechki qorong'i rejimga o'tdi. Sahifa qaysi temada bo'ladi? apply() chaqiriladimi?

Javob

Sahifa yorug' qoladi — qo'lda tanlov tizimdan ustun (data-tema="yorug" → color-scheme: light). apply() esa chaqiriladi, chunki change tinglovchisi doim ulangan. U hech narsani buzmaydi: visibleTheme() tanlovni qaytaradi va hamma narsa avvalgidek qoladi.

6. Harakatni kamaytirish

6.1 JavaScript animatsiyalari ham so'rashi kerak

prefers-reduced-motion darsida CSS animatsiyalarini o'chirishni o'rgandik. Lekin JavaScript animatsiyasini CSS to'xtata olmaydi: requestAnimationFrame sikli, karusel taymeri, scrollIntoView({ behavior: "smooth" }), video autoplay. Ular sozlamani o'zi so'rashi kerak.

requestAnimationFrame darsidagi buyurtmalar hisoblagichini eslang: raqam 1 soniyada 0 dan 1 284 gacha "yuguradi". O'shanda "harakatni kamaytirish yoqilgan odamga yakuniy raqamni darhol ko'rsatamiz" degan edik:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  .counter { font: 700 3rem/1.2 ui-monospace, monospace; margin: 0; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<p>Bugun «Bahor»da buyurtmalar:</p>
<p class="counter" id="counter">0</p>
<button id="again" type="button">Qayta sanash</button>
<script>
  const counter = document.getElementById("counter");
  const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)");
  const target = 1284;
  const duration = 1000;

  function formatNumber(n) {
    return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
  }

  function start() {
    if (reduceMotion.matches) {
      counter.textContent = formatNumber(target); // darhol
      console.log("Harakatsiz: 1 284");
      return;
    }
    let startTime = null;
    function step(time) {
      startTime ??= time;
      const progress = Math.min((time - startTime) / duration, 1);
      const value = Math.round(progress * target);
      counter.textContent = formatNumber(value);
      if (progress < 1) requestAnimationFrame(step);
      else console.log("Animatsiya bilan: 1 284");
    }
    requestAnimationFrame(step);
  }

  document.getElementById("again").addEventListener("click", start);
  start();
</script>

Konsolda:

text
Animatsiya bilan: 1 284

Bu — sozlama o'chiq bo'lgan oddiy holat. Biz xuddi shu blokni prefers-reduced-motion: reduce emulyatsiyasi bilan ham ochdik: raqam darhol 1 284 bo'ldi va konsolda Harakatsiz: 1 284. O'zingiz DevTools → Rendering → Emulate CSS media feature prefers-reduced-motion → reduce ni tanlab, «Qayta sanash» ni bosing.

reduceMotion.matches har start() da qayta o'qiladi. Shuning uchun change tinglovchisi shart emas: keyingi bosishda yangi sozlama o'zi hisobga olinadi. Tinglovchi faqat ishlab turgan harakatni to'xtatish kerak bo'lganda kerak — keyingi bo'limdagi karusel kabi.

6.2 "Kamaytirish" — o'chirish degani emas

Sozlama "kamroq harakat" deydi, "hech qanday o'zgarish yo'q" emas. Hisoblagichda yakuniy raqam baribir ko'rinadi. Karusel to'xtaydi, lekin tugmalar bilan varaqlanadi. Silliq aylantirish o'rniga sahifa darhol sakraydi:

js
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)");

section.scrollIntoView({
  behavior: reduceMotion.matches ? "auto" : "smooth",
});

Muhim ma'lumot harakat ichida bo'lmasin. Odam animatsiyani ko'rmasa ham, natijani ko'rishi kerak.

6.3 Ishlab turgan harakatni to'xtatish

Karusel har 5 soniyada aylanadi. Mehmon sahifa ochiq turganda sozlamani yoqdi. Endi change kerak:

js
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)");
let timerId = null;

function updateAutoplay() {
  clearInterval(timerId);
  if (!reduceMotion.matches) {
    timerId = setInterval(nextSlide, 5000);
  }
}

reduceMotion.addEventListener("change", updateAutoplay);
updateAutoplay(); // boshlang'ich holat

Ikki narsaga qarang. updateAutoplay ham boshida, ham har o'zgarishda chaqiriladi — bitta funksiya, ikki chaqiruv. Va u avval har doim eski taymerni to'xtatadi — ikki taymer parallel aylanib qolmaydi. prefers-reduced-motion darsidagi autoplay li fon videosi ham shunday to'xtatiladi: video.pause() va play().

Tekshirib ko'ring: Hisoblagichda change tinglovchisi yo'q, karuselda bor. Nega?

Javob

Hisoblagich bir martalik: sozlama har start() da qayta o'qiladi, keyingi bosishda yangi qiymat ishlaydi. Karusel esa to'xtovsiz ishlaydi: sozlama yoqilgan paytda u allaqachon aylanyapti. Uni to'xtatish uchun o'zgarish paytini bilish kerak — change.

7. JavaScript yoki CSS?

Qoida ResizeObserver darsidagidek: ko'rinish — CSS'da, qaror — JavaScript'da.

Vazifa Qayerda
Rang, o'lcham, joylashuv ekranga qarab CSS @media
Qorong'i rejim ranglari CSS light-dark()
Taymer, rAF, karuselni to'xtatish JS matchMedia
theme-color, canvas rangi, JS holati JS matchMedia

JavaScript'dagi media query CSS'dagini takrorlasa, 40rem soni ikki joyda yoziladi. Biri o'zgarsa, ikkinchisi unutiladi — CSS "tor" desa, JavaScript "keng" deydi. Raqamni JavaScript'da bitta o'zgarmasda saqlang (const WIDE_QUERY = "(min-width: 40rem)") va CSS'dagisi bilan bir xilligini izohda yozing.

8. Node'da yo'q

matchMedia — brauzerning ekrani bilan bog'liq, Node'da ekran yo'q:

js
const wide = matchMedia("(min-width: 40rem)");

Node 24 da:

text
ReferenceError: matchMedia is not defined

Tarjimasi: "matchMedia aniqlanmagan". Shuning uchun vazifalar dagi tema kodi ikki qismga bo'lingan: sof funksiyalar (Node'da testlanadi) va DOM'ga tegadigan qism (faqat brauzerda) — "Vazifalar qadami" da ko'rasiz.

9. Ko'p uchraydigan xatolar

9.1 Faqat change ga tayanish

js
// ❌ sahifa ochilganda hech narsa bo'lmaydi
wide.addEventListener("change", (e) => show(e.matches));

change — faqat o'zgarishda. Sahifa keng ekranda ochilib, hech qachon toraytirilmasa — show hech qachon chaqirilmaydi. Tuzatish: tinglovchidan keyin bir marta show(wide.matches).

9.2 Qavssiz savol

matchMedia("min-width: 600px") — xato yo'q, media — "not all", matches doim false. Tuzatish: xususiyat doim qavs ichida: "(min-width: 600px)".

9.3 resize + innerWidth

js
// ❌ har piksel o'zgarishida ishlaydi
addEventListener("resize", () => {
  sidebar.hidden = innerWidth < 640;
});

Ishlaydi, lekin keraksiz tez-tez. Ustiga, 640 piksel CSS'dagi 40rem bilan har doim teng emas: foydalanuvchi brauzer shriftini kattalashtirsa, rem ham kattalashadi. Tuzatish: matchMedia("(min-width: 40rem)") — CSS bilan aynan bir xil o'lchov.

9.4 Faqat tizimga qarash

prefers-color-scheme — tizim sozlamasi. Saytda qo'lda tanlov bo'lsa, faqat matchMedia ga qarab rang tanlagan kod (canvas, theme-color) qo'lda tanlovni e'tiborsiz qoldiradi. Tuzatish: "ko'rinadigan tema" ni bitta funksiyada hisoblang — visibleTheme() kabi — va hamma joyda shuni ishlating.

9.5 Sozlamani "majburlash"

Harakatni kamaytirgan odamga "Animatsiyani yoqing" degan xabar chiqarish yoki sozlamani e'tiborsiz qoldirish — yo'q. Bu odam uchun bosh aylanishi va ko'ngil aynishi bo'lishi mumkin (harakat etikasi). Sozlama — foydalanuvchining qarori.

10. Mashqlar

1-mashq (oson): Sichqoncha yoki barmoq

Sahifada «Bahor» menyusi uchun maslahat chiqsin: sichqonchali qurilmada Taom ustiga olib boring — tarkibi chiqadi, barmoqli qurilmada Taomni bosing — tarkibi chiqadi. Savol: (hover: hover) and (pointer: fine) («Zamonaviy diapazon sintaksisi» bo'limi). Matnni #hint ga yozing.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
</style>
<p id="hint">…</p>
<script>
  const mouse = matchMedia("(hover: hover) and (pointer: fine)");
  const hint = document.getElementById("hint");

  function showHint() {
    hint.textContent = mouse.matches
      ? "Taom ustiga olib boring — tarkibi chiqadi"
      : "Taomni bosing — tarkibi chiqadi";
  }

  mouse.addEventListener("change", showHint);
  showHint();
</script>

Kompyuterda birinchi matn, telefonda ikkinchisi. DevTools'ning qurilma rejimida (Ctrl + Shift + M) telefonni tanlab sahifani yangilang — matn almashadi. change tinglovchisi ham bor: planshetga sichqoncha ulansa, matn o'zi yangilanadi.

2-mashq (o'rta): Yumshoq aylantirish tugmasi

«Bahor» menyu sahifasida «Bronga o'tish» tugmasi pastdagi #bron bo'limiga aylantiradi. scrollIntoView ga behavior ni sozlamaga qarab bering ("Kamaytirish — o'chirish degani emas" bo'limi). Har bosishda konsolga qaysi usul ishlatilganini chiqaring: aylantirish: smooth yoki aylantirish: auto.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  .gap { height: 900px;
    background: linear-gradient(#f7faf8, #e8f1ec); }
  button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<button id="go" type="button">Bronga o'tish</button>
<div class="gap"></div>
<h2 id="bron">Stol bron qilish</h2>
<script>
  const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)");

  document.getElementById("go").addEventListener("click", () => {
    const behavior = reduceMotion.matches ? "auto" : "smooth";
    document.getElementById("bron").scrollIntoView({ behavior });
    console.log(`aylantirish: ${behavior}`);
  });
</script>

Tugmani bosing — konsolda aylantirish: smooth, sahifa silliq pastga tushadi (bosib tekshirdik). DevTools'da reduce ni yoqing va yana bosing — aylantirish: auto, sahifa darhol sakraydi. Sozlama har bosishda qayta o'qiladi, shuning uchun change kerak emas.

3-mashq (qiyin): watchMedia yordamchisi

Har safar ikki qator yozamiz: addEventListener("change", ...) va boshlang'ich chaqiruv. Bitta yordamchi yozing: watchMedia(query, callback) — callback(matches) ni darhol va har o'zgarishda chaqirsin, va tinglashni to'xtatadigan funksiya qaytarsin. Sinov: (min-width: 40rem) ni kuzating, natijani #state ga yozing, «To'xtatish» tugmasi kuzatuvni to'xtatsin va konsolga kuzatuv to'xtadi desin.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<p>Holat: <b id="state">…</b></p>
<button id="stop" type="button">To'xtatish</button>
<script>
  function watchMedia(query, callback) {
    const mq = matchMedia(query);
    const handler = (event) => callback(event.matches);
    mq.addEventListener("change", handler);
    callback(mq.matches); // boshlang'ich holat
    return () => mq.removeEventListener("change", handler);
  }

  const state = document.getElementById("state");
  const stop = watchMedia("(min-width: 40rem)", (isWide) => {
    state.textContent = isWide ? "keng" : "tor";
  });

  document.getElementById("stop").addEventListener("click", () => {
    stop();
    console.log("kuzatuv to'xtadi");
  });
</script>

360 va 1280 tugmalari bilan oynani almashtiring — holat o'zgaradi. «To'xtatish» dan keyin almashtirsangiz — holat qotib qoladi. Biz tekshirdik: to'xtatishdan oldin 360 px da tor, to'xtatgandan keyin 1280 px da ham tor qoldi.

handler alohida o'zgaruvchida — removeEventListener ga aynan o'sha funksiya kerak. React'dagi "media query hook'lari" ham ichida shuni qiladi (React — 17-qismda).

4-mashq: Vazifalar qadami — tema tanlovi

vazifalar ga 13-qismning ikkinchi qadami: uch holatli tema (tizim, yorug', qorong'i). Bu «Bahor»da qurgan naqshning o'zi, faqat modul ko'rinishida va testlar bilan. Qo'shimcha — prefers-reduced-motion CSS'da hurmat qilinadi.

1. Branch:

bash
git switch -c feature/tema

2. index.html, <head> da — ikki meta va oddiy skript (CSS'dan oldin):

html
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#f7faf8">
<script src="assets/js/tema.js"></script>
<link rel="stylesheet" href="assets/css/vazifalar.css">

<body> da, import formasi ostida — «Bahor»dagi radio guruh, faqat id tema-tanlov va legend Tema. modulepreload ro'yxatiga tema-tanlash.js qo'shiladi.

3. assets/js/tema.js — chizishdan oldin ishlaydigan oddiy skript:

js
try {
  const tema = localStorage.getItem("vazifalar:tema");
  if (tema === "yorug" || tema === "qorongi") {
    document.documentElement.dataset.tema = tema;
  }
} catch {
  // localStorage yopiq — tizim rejimi qoladi
}

U modul emas: modul kechiktiriladi, bu skript esa sahifa chizilishidan oldin ishlashi shart — aks holda sahifa bir lahza tizim rangida "chaqnaydi" (Qorong'i rejim darsidagi FOUC). Shu sababli u saqlash.js ni import qila olmaydi. Bu loyihadagi yagona joy: localStorage to'g'ridan-to'g'ri o'qiladi (kanonda bu istisno TEXNIK-QARZ ga yozilgan).

4. saqlash.js — kalit "vazifalar:tema":

js
// "tizim" — yozuv o'chadi: tanlov yo'q, tizim sozlamasi ishlaydi
export function temaniSaqla(tema) {
  try {
    if (tema === "tizim") {
      localStorage.removeItem(TEMA_KALITI);
    } else {
      localStorage.setItem(TEMA_KALITI, tema);
    }
  } catch (xato) {
    console.warn(`Tema saqlanmadi: ${xato.name}`);
  }
}

export function temaniOl() {
  try {
    return localStorage.getItem(TEMA_KALITI);
  } catch {
    return null;
  }
}

5. Yangi modul assets/js/tema-tanlash.js — ikki qism. Sof funksiyalar (DOM'siz, Node'da testlanadi):

js
export const TEMALAR = Object.freeze(["tizim", "yorug", "qorongi"]);

// Xotiradagi qiymat qo'lda o'zgartirilgan bo'lishi mumkin
export function tanlovniOl(qiymat) {
  return TEMALAR.includes(qiymat) ? qiymat : "tizim";
}

// Ekranda qaysi tema ko'rinadi: "tizim" — tizim sozlamasiga qarab
export function korinadiganTema(tanlov, tizimQorongimi) {
  if (tanlov !== "tizim") {
    return tanlov;
  }
  return tizimQorongimi ? "qorongi" : "yorug";
}

Va DOM'ga tegadigan temaniUla() — u import paytida emas, chaqirilganda ishlaydi. Shuning uchun Node'dagi test faylni import qilganda matchMedia is not defined xatosi chiqmaydi:

js
export function temaniUla() {
  const guruh = document.getElementById("tema-tanlov");
  const panel = document.querySelector('meta[name="theme-color"]');
  const tizimQorongi = matchMedia("(prefers-color-scheme: dark)");
  let tanlov = tanlovniOl(temaniOl());

  function qolla() {
    const html = document.documentElement;
    if (tanlov === "tizim") {
      delete html.dataset.tema;
    } else {
      html.dataset.tema = tanlov;
    }
    const tema = korinadiganTema(tanlov, tizimQorongi.matches);
    panel.content = PANEL_RANGLARI[tema];
    guruh.elements.namedItem(`tema-${tanlov}`).checked = true;
  }

  guruh.addEventListener("change", (event) => {
    tanlov = tanlovniOl(event.target.value);
    temaniSaqla(tanlov);
    qolla();
  });
  // "Tizim" tanlanganda tizim o'zgarishi jonli kuzatiladi
  tizimQorongi.addEventListener("change", qolla);
  qolla();
  guruh.hidden = false; // JS ishlamasa — tugmalar ko'rinmaydi
}

Jonli misoldagi apply() bilan solishtiring: qolla — o'sha funksiya. Farqi — korinadiganTema alohida, sof funksiya bo'lib chiqdi: uni ikki argument bilan testlash mumkin. PANEL_RANGLARI — { yorug: "#f7faf8", qorongi: "#14201a" }, ilova fonining ikki rangi.

6. asosiy.js — ikki qator: import { temaniUla } from "./tema-tanlash.js"; va ishga tushirishda, render() dan oldin, temaniUla();.

7. vazifalar.css — ranglar light-dark() bilan, @supports ichida. Bu qoidani bilmaydigan eski brauzer ichkariga kirmaydi va yuqoridagi oddiy yorug' ranglarda qoladi:

css
@supports (color: light-dark(white, black)) {
  :root {
    color-scheme: light dark;
    --rang-fon: light-dark(#f7faf8, #14201a);
    --rang-matn: light-dark(#1f2a24, #e6efe9);
    /* … qolgan tokenlar ham shunday */
  }
  :root[data-tema="yorug"] { color-scheme: light; }
  :root[data-tema="qorongi"] { color-scheme: dark; }
}

Ikki yangi token — --rang-yuza (tugma foni) va --rang-asosiy-ustida (yashil tugmadagi matn): oldin u yerda #fff qattiq yozilgan edi va qorong'i rejimda oq dog' bo'lib qolardi. body ga transition: background-color 0.2s, color 0.2s — tema yumshoq almashadi. Va oxirida:

css
/* Harakatni kamaytirish so'ralgan: o'tishlar deyarli darhol */
@media (prefers-reduced-motion: reduce) {
  *, ::before, ::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

Nega 0 emas, 0.01ms? Nol davomiylikda o'tish umuman boshlanmaydi va transitionend hodisasi kelmaydi. Unga tayanadigan kod "o'tish tugashini" abadiy kutib qoladi. Chrome 154 da sinadik: 0s li elementda hodisa yo'q, 0.01ms lisida — bor. Qiymat ko'zga sezilmaydi, hodisa esa keladi.

8. Testlar. tekshiruv/tema.test.js — 4 ta: tanlovniOl (faqat uch qiymat, "dark" → "tizim"), korinadiganTema ikki holatda va temaniSaqla ("tizim" yozuvni o'chiradi). Natija — 70/70; lint, format:check, tip — toza.

9. Brauzerda (kanon tekshiruvi, 60/60 ning bir qismi):

text
✅ tema: tizim qorong'i — fon (CSS): "rgb(20, 32, 26)"
✅ tema: tizim qorong'i — panel (change hodisasi): "#14201a"
✅ tema: strelka → Yorug': ["tema-yorug","yorug","yorug"]
✅ tema: qayta ochilishda modul kelguncha qo'yilgan: "qorongi"
✅ tema: tizim — yozuv o'chdi: [null,false]
✅ tema: reduced-motion — o'tish yo'q: "1e-05s"

Ikkinchi qatorga qarang: tizim emulyatsiyada qorong'iga o'tganda panel rangi change hodisasi orqali yangilandi. Uchinchisi — klaviatura: radio guruhda strelka tanlovni almashtiradi (Bobur uchun). To'rtinchisi — tema.js ishi: modul hali yuklanmagan paytda data-tema allaqachon qo'yilgan. Oxirgisi — 0.01ms ning brauzer ko'rsatadigan shakli.

O'zingiz: Live Server'da oching, DevTools → Rendering'da prefers-color-scheme: dark ni yoqing — «Tizim» tanlangan ilova qorong'iga o'tadi. «Yorug'» ni tanlang va emulyatsiyani almashtiring — ilova yorug' qoladi.

10. Commit va PR. To'liq xabar 107 belgi — Git sarlavhasi uchun uzun (≤ 72). Shuning uchun ikkiga bo'lamiz — sarlavha va tana:

bash
git add index.html assets/js/tema.js assets/js/tema-tanlash.js \
  assets/js/saqlash.js assets/js/asosiy.js \
  assets/css/vazifalar.css tekshiruv/tema.test.js
git commit \
  -m "feat: tizim/yorug'/qorong'i tema — matchMedia, prefers-color-scheme" \
  -m "prefers-reduced-motion hurmat qilinadi"
git push -u origin feature/tema
gh pr create --fill
gh pr merge --merge

Diff: 7 fayl, +180 −3.

11. Real ishda

  • Tema tanlovi — GitHub, YouTube, Telegram Web, VS Code: deyarli hammasida "Tizim / Yorug' / Qorong'i" uchligi. Next.js (React ustiga qurilgan freymvork, kursda keyinroq) loyihalarida next-themes kutubxonasi aynan shu mantiqni (saqlash, FOUC skripti, matchMedia) tayyor beradi.
  • Animatsiya kutubxonalari — GSAP'ning gsap.matchMedia(), Motion (avvalgi nomi Framer Motion) kutubxonasining useReducedMotion i — ichida shu matchMedia.
  • Moslashuvchan komponentlar — "telefonda pastki panel, kompyuterda yon menyu" holati JavaScript'da kerak bo'lganda. React'da bunday hook'lar keng tarqalgan (React — 17-qismda).
  • Qulaylik (accessibility) auditi — qo'lda tekshiruvda "harakatni kamaytirish hurmat qilinadimi?" degan band bor (WCAG 2.3.3 mezoni). Lighthouse buni avtomatik tekshirmaydi — DevTools emulyatsiyasi bilan o'zingiz sinaysiz.
  • Intervyu. "resize va matchMedia farqi?", "Qorong'i rejimni qanday qilasiz, FOUC'dan qanday qochasiz?" — frontend intervyularida tez-tez.

Xulosa

  • matchMedia(query) → MediaQueryList: matches — hozirgi javob, media — tushunilgan savol ("not all" — sintaksis xato).
  • change faqat javob almashganda keladi — resize dan tejamkor; boshlang'ich holatni bir marta qo'lda qo'llang.
  • prefers-color-scheme, prefers-reduced-motion, prefers-contrast, forced-colors — foydalanuvchi sozlamalari; DevTools Rendering bilan sinanadi.
  • Tema: tanlov saqlanadi, "tizim" — yozuv yo'q; ko'rinadigan tema bitta funksiyada; theme-color JavaScript'dan.
  • JavaScript animatsiyasi sozlamani o'zi so'raydi; ishlab turgan harakat change da to'xtatiladi.

Keyingi dars: Clipboard va Web Share API — «Nusxalash» tugmasi va telefonning "Ulashish" oynasi bir necha qatorda.

Manbalar

  • CSSWG: "CSSOM View Module" — matchMedia, MediaQueryList — drafts.csswg.org/cssom-view
  • CSSWG: "Media Queries Level 5" — user preference media features — drafts.csswg.org/mediaqueries-5
  • MDN: Window.matchMedia(), MediaQueryList, prefers-reduced-motion — developer.mozilla.org
  • web.dev: "prefers-reduced-motion: Sometimes less movement is more" — web.dev
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript matchMedia: media query'lar va foydalanuvchi sozlamalari — IlmHamroh